UNPKG

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

Version:
697 lines (673 loc) 23.4 kB
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