UNPKG

@openzeppelin/contracts-ui-builder-adapter-midnight

Version:
727 lines (702 loc) 26.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.ts var index_exports = {}; __export(index_exports, { MidnightAdapter: () => MidnightAdapter, default: () => adapter_default, isMidnightContractArtifacts: () => isMidnightContractArtifacts, midnightAdapterConfig: () => midnightAdapterConfig, midnightNetworks: () => midnightNetworks, midnightTestnet: () => midnightTestnet, midnightTestnetNetworks: () => midnightTestnetNetworks }); module.exports = __toCommonJS(index_exports); // src/configuration/execution.ts var import_contracts_ui_builder_utils2 = require("@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 var import_contracts_ui_builder_utils = require("@openzeppelin/contracts-ui-builder-utils"); // src/configuration/explorer.ts var import_contracts_ui_builder_utils3 = require("@openzeppelin/contracts-ui-builder-utils"); // src/configuration/rpc.ts var import_contracts_ui_builder_utils4 = require("@openzeppelin/contracts-ui-builder-utils"); function validateMidnightRpcEndpoint(_rpcConfig) { import_contracts_ui_builder_utils4.logger.info("validateMidnightRpcEndpoint", "Midnight RPC validation not yet implemented"); return true; } async function testMidnightRpcConnection(_rpcConfig) { import_contracts_ui_builder_utils4.logger.info("testMidnightRpcConnection", "TODO: Implement RPC connection testing"); return { success: true }; } // src/adapter.ts var import_contracts_ui_builder_types = require("@openzeppelin/contracts-ui-builder-types"); var import_contracts_ui_builder_utils8 = require("@openzeppelin/contracts-ui-builder-utils"); // src/wallet/components/account/AccountDisplay.tsx var import_lucide_react = require("lucide-react"); var import_contracts_ui_builder_ui = require("@openzeppelin/contracts-ui-builder-ui"); var import_contracts_ui_builder_utils5 = require("@openzeppelin/contracts-ui-builder-utils"); // src/wallet/hooks/useMidnightWallet.ts var import_react2 = require("react"); // src/wallet/context/MidnightWalletContext.tsx var import_react = require("react"); var MidnightWalletContext = (0, import_react.createContext)( void 0 ); // src/wallet/hooks/useMidnightWallet.ts var useMidnightWallet = () => { const context = (0, import_react2.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 var import_react3 = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var SafeMidnightComponent = ({ children, fallback = null }) => { const context = (0, import_react3.useContext)(MidnightWalletContext); if (!context) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback }); } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children }); }; // src/wallet/components/account/AccountDisplay.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var CustomAccountDisplay = ({ className }) => { return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SafeMidnightComponent, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AccountDisplayContent, { className }) }); }; var AccountDisplayContent = ({ className }) => { const { address, isConnected } = useAccount(); const { disconnect: disconnect2 } = useDisconnect(); if (!isConnected || !address || !disconnect2) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: (0, import_contracts_ui_builder_utils5.cn)("flex items-center gap-2", className), children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-xs font-medium", children: (0, import_contracts_ui_builder_utils5.truncateMiddle)(address, 4, 4) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[9px] text-muted-foreground -mt-0.5", children: "Midnight Network" }) ] }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_contracts_ui_builder_ui.Button, { onClick: () => disconnect2(), variant: "ghost", size: "icon", className: "size-6 p-0", title: "Disconnect wallet", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.LogOut, { className: "size-3.5" }) } ) ] }); }; // src/wallet/components/connect/ConnectButton.tsx var import_lucide_react2 = require("lucide-react"); var import_contracts_ui_builder_ui2 = require("@openzeppelin/contracts-ui-builder-ui"); var import_contracts_ui_builder_utils6 = require("@openzeppelin/contracts-ui-builder-utils"); var import_jsx_runtime3 = require("react/jsx-runtime"); var ConnectButton = ({ className, hideWhenConnected = true }) => { const unavailableButton = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: (0, import_contracts_ui_builder_utils6.cn)("flex items-center", className), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_contracts_ui_builder_ui2.Button, { disabled: true, variant: "outline", size: "sm", className: "h-8 px-2 text-xs", children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Wallet, { className: "size-3.5 mr-1" }), "Wallet Unavailable" ] }) }); return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SafeMidnightComponent, { fallback: unavailableButton, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(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__ */ (0, import_jsx_runtime3.jsxs)("div", { className: (0, import_contracts_ui_builder_utils6.cn)("flex flex-col items-start gap-1", className), children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)( import_contracts_ui_builder_ui2.Button, { onClick: handleConnectClick, disabled: showButtonLoading || isConnected || !hasWallet, variant: "outline", size: "sm", className: "h-8 px-2 text-xs", children: [ showButtonLoading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "size-3.5 animate-spin mr-1" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Wallet, { className: "size-3.5 mr-1" }), buttonText ] } ) }), connectError && !isConnecting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-red-500 px-2", children: connectError.message || "Error connecting wallet" }) ] }); }; // src/wallet/components/MidnightWalletProvider.tsx var import_react4 = require("react"); var import_contracts_ui_builder_utils7 = require("@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 var import_jsx_runtime4 = require("react/jsx-runtime"); var MidnightWalletProvider = ({ children }) => { const [api, setApi] = (0, import_react4.useState)(null); const [error, setError] = (0, import_react4.useState)(null); const [address, setAddress] = (0, import_react4.useState)(void 0); const [isConnecting, setIsConnecting] = (0, import_react4.useState)(false); const [isInitializing, setIsInitializing] = (0, import_react4.useState)(true); const pollIntervalRef = (0, import_react4.useRef)(null); const isConnected = !!address && !!api; const cleanupTimer = (0, import_react4.useCallback)(() => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }, []); (0, import_react4.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) { import_contracts_ui_builder_utils7.logger.warn("MidnightWalletProvider", "Auto-reconnect failed:", err); } finally { setIsInitializing(false); } }; attemptAutoConnect(); }, []); const handleConnect = (0, import_react4.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 = (0, import_react4.useCallback)(async () => { disconnect(); setApi(null); setAddress(void 0); setError(null); cleanupTimer(); localStorage.setItem("midnight-hasExplicitlyDisconnected", "true"); }, [cleanupTimer]); (0, import_react4.useEffect)(() => { return cleanupTimer; }, [cleanupTimer]); (0, import_react4.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 = (0, import_react4.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__ */ (0, import_jsx_runtime4.jsx)(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 (!(0, import_contracts_ui_builder_types.isMidnightNetworkConfig)(networkConfig)) { throw new Error("MidnightAdapter requires a valid Midnight network configuration."); } this.networkConfig = networkConfig; this.initialAppServiceKitName = "custom"; import_contracts_ui_builder_utils8.logger.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) { import_contracts_ui_builder_utils8.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." }); } 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; import_contracts_ui_builder_utils8.logger.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) { import_contracts_ui_builder_utils8.logger.warn("MidnightAdapter", "getRelayers is not implemented for the Midnight adapter yet."); return Promise.resolve([]); } async getRelayer(_serviceUrl, _accessToken, _relayerId) { import_contracts_ui_builder_utils8.logger.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" } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MidnightAdapter, isMidnightContractArtifacts, midnightAdapterConfig, midnightNetworks, midnightTestnet, midnightTestnetNetworks }); //# sourceMappingURL=index.cjs.map