diff --git a/.github/docs-gen-prompts.md b/.github/docs-gen-prompts.md new file mode 100644 index 00000000..ca24b27c --- /dev/null +++ b/.github/docs-gen-prompts.md @@ -0,0 +1,131 @@ +# AI Documentation Enhancement Prompts + +--- + +## System Prompt + +You are the Compose Solidity documentation orchestrator. Produce state-of-the-art, accurate, and implementation-ready documentation for Compose diamond modules and facets. Always respond with valid JSON only (no markdown). Follow all appended guideline sections from `copilot-instructions.md`, Compose conventions, and the templates below. + +- Audience: Solidity engineers building on Compose diamonds. Prioritize clarity, precision, and developer actionability. +- Grounding: Use only the provided contract data. Do not invent functions, storage layouts, events, errors, modules, or behaviors. Keep terminology aligned with Compose (diamond proxy, facets, modules, storage pattern). +- Tone and style: Active voice, concise sentences, zero fluff/marketing. Prefer imperative guidance over vague descriptions. +- Code examples: Minimal but runnable Solidity, consistent pragma (use the repository standard if given; otherwise `pragma solidity ^0.8.30;`). Import and call the actual functions exactly as named. Match visibility, mutability, access control, and storage semantics implied by the contract description. +- Output contract details only through the specified JSON fields. Do not add extra keys or reorder fields. Escape newlines as `\\n` inside JSON strings. + +### Quality Guardrails (must stay in the system prompt) + +- Hallucinations: no invented APIs, behaviors, dependencies, or storage details beyond the supplied context. +- Vagueness and filler: avoid generic statements like “this is very useful”; be specific to the module/facet and diamond pattern. +- Repetition and redundancy: do not restate inputs verbatim or repeat the same idea in multiple sections. +- Passive, wordy, or hedging language: prefer direct, active phrasing without needless qualifiers. +- Inaccurate code: wrong function names/params/visibility, missing imports, or examples that can’t compile. +- Inconsistency: maintain a steady tense, voice, and terminology; keep examples consistent with the described functions. +- Overclaiming: no security, performance, or compatibility claims that are not explicitly supported by the context. + +--- + +## Relevant Guideline Sections + +These section headers from `copilot-instructions.md` are appended to the system prompt to enforce Compose-wide standards. One section per line; must match exactly. + +``` +## 3. Core Philosophy +## 4. Facet Design Principles +## 5. Banned Solidity Features +## 6. Composability Guidelines +## 11. Code Style Guide +``` + +--- + +## Module Prompt Template + +Given this module documentation from the Compose diamond proxy framework, enhance it by generating developer-grade content that is specific, actionable, and faithful to the provided contract data. + +1. **description**: A concise one-line description (max 100 chars) for the page subtitle. Derive from the module's purpose based on its functions and NatSpec. Do NOT include "module" or "for Compose diamonds" - just describe what it does. +2. **overview**: 2-3 sentence overview of what the module does and why it matters for diamonds (storage reuse, composition, safety). +3. **usageExample**: 10-20 lines of Solidity demonstrating how a facet would import and call this module. Use the real function names and signatures; include pragma and any required imports. Keep it minimal but compilable. +4. **bestPractices**: 2-3 bullets focused on safe and idiomatic use (access control, storage hygiene, upgrade awareness, error handling). +5. **integrationNotes**: Explain how the module interacts with diamond storage and how changes are visible to facets; note any invariants or ordering requirements. +6. **keyFeatures**: 2-4 bullets highlighting unique capabilities, constraints, or guarantees. + +Contract Information: +- Name: {{title}} +- Current Description: {{description}} +- Functions: {{functionNames}} +- Events: {{eventNames}} +- Errors: {{errorNames}} +- Function Details: +{{functionDescriptions}} + +Respond ONLY with valid JSON in this exact format (no markdown code blocks, no extra text): +{ + "description": "concise one-line description here", + "overview": "enhanced overview text here", + "usageExample": "solidity code here (use \\n for newlines)", + "bestPractices": "- Point 1\\n- Point 2\\n- Point 3", + "keyFeatures": "- Feature 1\\n- Feature 2", + "integrationNotes": "integration notes here" +} + +--- + +## Facet Prompt Template + +Given this facet documentation from the Compose diamond proxy framework, enhance it by generating precise, implementation-ready guidance. + +1. **description**: A concise one-line description (max 100 chars) for the page subtitle. Derive from the facet's purpose based on its functions and NatSpec. Do NOT include "facet" or "for Compose diamonds" - just describe what it does. +2. **overview**: 2-3 sentence summary of the facet's purpose and value inside a diamond (routing, orchestration, surface area). +3. **usageExample**: 10-20 lines showing how this facet is deployed or invoked within a diamond. Include pragma, imports, selector usage, and sample calls that reflect the real function names and signatures. +4. **bestPractices**: 2-3 bullets on correct integration patterns (initialization, access control, storage handling, upgrade safety). +5. **securityConsiderations**: Concise notes on access control, reentrancy, input validation, and any state-coupling risks specific to this facet. +6. **keyFeatures**: 2-4 bullets calling out unique abilities, constraints, or guarantees. + +Contract Information: +- Name: {{title}} +- Current Description: {{description}} +- Functions: {{functionNames}} +- Events: {{eventNames}} +- Errors: {{errorNames}} +- Function Details: +{{functionDescriptions}} + +Respond ONLY with valid JSON in this exact format (no markdown code blocks, no extra text): +{ + "description": "concise one-line description here", + "overview": "enhanced overview text here", + "usageExample": "solidity code here (use \\n for newlines)", + "bestPractices": "- Point 1\\n- Point 2\\n- Point 3", + "keyFeatures": "- Feature 1\\n- Feature 2", + "securityConsiderations": "security notes here" +} + +--- + +## Module Fallback Content + +Used when AI enhancement is unavailable for modules. + +### integrationNotes + +This module accesses shared diamond storage, so changes made through this module are immediately visible to facets using the same storage pattern. All functions are internal as per Compose conventions. + +### keyFeatures + +- All functions are `internal` for use in custom facets +- Follows diamond storage pattern (EIP-8042) +- Compatible with ERC-2535 diamonds +- No external dependencies or `using` directives + +--- + +## Facet Fallback Content + +Used when AI enhancement is unavailable for facets. + +### keyFeatures + +- Self-contained facet with no imports or inheritance +- Only `external` and `internal` function visibility +- Follows Compose readability-first conventions +- Ready for diamond integration diff --git a/.github/scripts/ai-provider/README.md b/.github/scripts/ai-provider/README.md new file mode 100644 index 00000000..58ad2218 --- /dev/null +++ b/.github/scripts/ai-provider/README.md @@ -0,0 +1,179 @@ +# AI Provider Service + +Simple, configurable AI service for CI workflows supporting multiple providers. + +## Features + +- **Simple API**: One function to call any AI model +- **Multiple Providers**: GitHub Models (GPT-4o) and Google Gemini +- **Auto-detection**: Automatically uses available provider +- **Rate Limiting**: Built-in request and token-based rate limiting +- **Configurable**: Override provider and model via environment variables + +## Supported Providers + +| Provider | Models | Rate Limits | API Key | +|----------|--------|-------------|---------| +| **GitHub Models** | gpt-4o, gpt-4o-mini | 10 req/min, 40k tokens/min | `GITHUB_TOKEN` | +| **Google Gemini** | gemini-1.5-flash, gemini-1.5-pro | 15 req/min, 1M tokens/min | `GOOGLE_AI_API_KEY` | + +## Usage + +### Basic Usage + +```javascript +const ai = require('./ai-provider'); + +const response = await ai.call( + 'You are a helpful assistant', // system prompt + 'Explain quantum computing' // user prompt +); + +console.log(response); +``` + +### With Options + +```javascript +const response = await ai.call( + systemPrompt, + userPrompt, + { + maxTokens: 1000, + onSuccess: (text, tokens) => { + console.log(`Success! Used ${tokens} tokens`); + }, + onError: (error) => { + console.error('Failed:', error); + } + } +); +``` + +## Environment Variables + +### Provider Selection + +```bash +# Auto-detect (default) - Try other provider with fallback to Github +AI_PROVIDER=auto + +# Use specific provider +AI_PROVIDER=github # Use GitHub Models +AI_PROVIDER=gemini # Use Google Gemini +``` + +### Model Override + +```bash +# Override default model for the provider +AI_MODEL=gpt-4o # For GitHub Models +AI_MODEL=gemini-1.5-pro # For Gemini +``` + +### API Keys + +```bash +# Google Gemini +GOOGLE_AI_API_KEY= +``` + +## Examples + +## GitHub Actions Integration + +```yaml +- name: Run AI-powered task + env: + # Option 1: Auto-detect (recommended) + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} + + # Option 2: Force specific provider + # AI_PROVIDER: 'gemini' + # AI_MODEL: 'gemini-1.5-pro' + run: node .github/scripts/your-script.js +``` + +## Architecture + +``` +ai-provider/ +├── index.js # Main service (singleton) +├── base-provider.js # Base provider class +├── provider-factory.js # Provider creation logic +├── rate-limiter.js # Rate limiting logic +└── providers/ + ├── github-models.js # GitHub Models implementation + └── gemini.js # Gemini implementation +``` + +## Adding a New Provider + +1. Create a new provider class in `providers/`: + +```javascript +const BaseAIProvider = require('../base-provider'); + +class MyProvider extends BaseAIProvider { + constructor(config, apiKey) { + super('My Provider', config, apiKey); + } + + buildRequestOptions() { + // Return HTTP request options + } + + buildRequestBody(systemPrompt, userPrompt, maxTokens) { + // Return JSON.stringify(...) of request body + } + + extractContent(response) { + // Return { content: string, tokens: number|null } + } +} + +module.exports = MyProvider; +``` + +2. Register in `provider-factory.js`: + +```javascript +const MyProvider = require('./providers/my-provider'); + +function createMyProvider(customModel) { + const apiKey = process.env.MY_PROVIDER_API_KEY; + if (!apiKey) return null; + + return new MyProvider({ model: customModel || 'default-model' }, apiKey); +} +``` + +3. Add to auto-detection or switch statement. + +## Rate Limiting + +The service automatically handles rate limiting: + +- **Request-based**: Ensures minimum delay between requests +- **Token-based**: Tracks token consumption in a 60-second rolling window +- **Smart waiting**: Calculates exact wait time needed + +Rate limits are provider-specific and configured automatically. + +## Error Handling + +```javascript +try { + const response = await ai.call(systemPrompt, userPrompt); + // Use response +} catch (error) { + if (error.message.includes('429')) { + console.log('Rate limited - try again later'); + } else if (error.message.includes('401')) { + console.log('Invalid API key'); + } else { + console.log('Other error:', error.message); + } +} +``` diff --git a/.github/scripts/ai-provider/index.js b/.github/scripts/ai-provider/index.js new file mode 100644 index 00000000..f982baaa --- /dev/null +++ b/.github/scripts/ai-provider/index.js @@ -0,0 +1,138 @@ +/** + * AI Provider Service + * Simple, configurable AI service supporting multiple providers + * + * Usage: + * const ai = require('./ai-provider'); + * const response = await ai.call(systemPrompt, userPrompt); + * + * Environment Variables: + * AI_PROVIDER - 'github' | 'gemini' | 'auto' (default: auto) + * AI_MODEL - Override default model + * GITHUB_TOKEN - For GitHub Models + * GOOGLE_AI_API_KEY - For Gemini + */ + +const { getProvider } = require('./provider-factory'); +const RateLimiter = require('./rate-limiter'); + +class AIProvider { + constructor() { + this.provider = null; + this.rateLimiter = new RateLimiter(); + this.initialized = false; + } + + /** + * Initialize the provider (lazy loading) + */ + _init() { + if (this.initialized) { + return; + } + + this.provider = getProvider(); + if (!this.provider) { + throw new Error( + 'No AI provider available. Set AI_PROVIDER or corresponding API key.' + ); + } + + console.log(`====================================================`); + console.log(` ✨ Using AI provider: ${this.provider.name}`); + console.log(`====================================================`); + + this.rateLimiter.setProvider(this.provider); + this.initialized = true; + } + + /** + * Make an AI call + * + * @param {string} systemPrompt - System prompt + * @param {string} userPrompt - User prompt + * @param {object} options - Optional settings + * @param {number} options.maxTokens - Override max tokens + * @param {function} options.onSuccess - Success callback + * @param {function} options.onError - Error callback + * @returns {Promise} Response text + */ + async call(systemPrompt, userPrompt, options = {}) { + this._init(); + + const { + maxTokens = null, + onSuccess = null, + onError = null, + } = options; + + if (!systemPrompt || !userPrompt) { + throw new Error('systemPrompt and userPrompt are required'); + } + + try { + // Estimate tokens and wait for rate limits + const tokensToUse = maxTokens || this.provider.getMaxTokens(); + const estimatedTokens = this.rateLimiter.estimateTokenUsage( + systemPrompt, + userPrompt, + tokensToUse + ); + + await this.rateLimiter.waitForRateLimit(estimatedTokens); + + // Build and send request + const requestBody = this.provider.buildRequestBody(systemPrompt, userPrompt, tokensToUse); + const requestOptions = this.provider.buildRequestOptions(); + + const response = await this._makeRequest(requestOptions, requestBody); + + // Extract content + const extracted = this.provider.extractContent(response); + if (!extracted) { + throw new Error('Invalid response format from API'); + } + + // Record actual token usage + const actualTokens = extracted.tokens || estimatedTokens; + this.rateLimiter.recordTokenConsumption(actualTokens); + + if (onSuccess) { + onSuccess(extracted.content, actualTokens); + } + + return extracted.content; + + } catch (error) { + console.error(` ❌ AI call failed: ${error.message}`); + + if (onError) { + onError(error); + } + + throw error; + } + } + + /** + * Make HTTPS request + */ + async _makeRequest(options, body) { + const { makeHttpsRequest } = require('../workflow-utils'); + return await makeHttpsRequest(options, body); + } + + /** + * Get provider info + */ + getProviderInfo() { + this._init(); + return { + name: this.provider.name, + limits: this.provider.getRateLimits(), + maxTokens: this.provider.getMaxTokens(), + }; + } +} + +module.exports = new AIProvider(); \ No newline at end of file diff --git a/.github/scripts/ai-provider/provider-factory.js b/.github/scripts/ai-provider/provider-factory.js new file mode 100644 index 00000000..12c47497 --- /dev/null +++ b/.github/scripts/ai-provider/provider-factory.js @@ -0,0 +1,65 @@ +/** + * Provider Factory + * Creates the appropriate AI provider based on environment variables + */ + +const { createGitHubProvider } = require('./providers/github-models'); +const { createGeminiProvider } = require('./providers/gemini'); + +/** + * Get the active AI provider based on environment configuration + * + * Environment variables: + * - AI_PROVIDER: 'github' | 'gemini' | 'auto' (default: 'auto') + * - AI_MODEL: Override default model for the provider + * - GITHUB_TOKEN: API key for GitHub Models + * - GOOGLE_AI_API_KEY: API key for Gemini + * + * @returns {BaseAIProvider|null} Provider instance or null if none available + */ +function getProvider() { + const providerName = (process.env.AI_PROVIDER || 'auto').toLowerCase(); + const customModel = process.env.AI_MODEL; + + if (providerName === 'auto') { + return autoDetectProvider(customModel); + } + + switch (providerName) { + case 'github': + case 'github-models': + return createGitHubProvider(customModel); + + case 'gemini': + case 'google': + return createGeminiProvider(customModel); + + default: + console.warn(`⚠️ Unknown provider: ${providerName}. Falling back to auto-detect.`); + return autoDetectProvider(customModel); + } +} + +/** + * Auto-detect provider based on available API keys + */ +function autoDetectProvider(customModel) { + // Try Gemini + const geminiProvider = createGeminiProvider(customModel); + if (geminiProvider) { + return geminiProvider; + } + + // Fallback to GitHub Models (free in GitHub Actions) + const githubProvider = createGitHubProvider(customModel); + if (githubProvider) { + return githubProvider; + } + + return null; +} + +module.exports = { + getProvider, +}; + diff --git a/.github/scripts/ai-provider/providers/base-provider.js b/.github/scripts/ai-provider/providers/base-provider.js new file mode 100644 index 00000000..fe23fb1c --- /dev/null +++ b/.github/scripts/ai-provider/providers/base-provider.js @@ -0,0 +1,68 @@ +/** + * Base AI Provider class + * All provider implementations should extend this class + */ +class BaseAIProvider { + constructor(name, config, apiKey) { + if (!apiKey) { + throw new Error('API key is required'); + } + + this.name = name; + this.config = config; + this.apiKey = apiKey; + } + + /** + * Get maximum output tokens for this provider + */ + getMaxTokens() { + return this.config.maxTokens || 2500; + } + + /** + * Get rate limits for this provider + */ + getRateLimits() { + return { + maxRequestsPerMinute: this.config.maxRequestsPerMinute || 10, + maxTokensPerMinute: this.config.maxTokensPerMinute || 40000, + }; + } + + /** + * Build HTTP request options + * Must be implemented by subclass + */ + buildRequestOptions() { + throw new Error('buildRequestOptions must be implemented by subclass'); + } + + /** + * Build request body with prompts + * Must be implemented by subclass + */ + buildRequestBody(systemPrompt, userPrompt, maxTokens) { + throw new Error('buildRequestBody must be implemented by subclass'); + } + + /** + * Extract content and token usage from API response + * Must be implemented by subclass + * @returns {{content: string, tokens: number|null}|null} + */ + extractContent(response) { + throw new Error('extractContent must be implemented by subclass'); + } + + /** + * Check if error is a rate limit error + */ + isRateLimitError(error) { + const msg = error?.message || ''; + return msg.includes('429') || msg.toLowerCase().includes('rate limit'); + } +} + +module.exports = BaseAIProvider; + diff --git a/.github/scripts/ai-provider/providers/gemini.js b/.github/scripts/ai-provider/providers/gemini.js new file mode 100644 index 00000000..9af4f159 --- /dev/null +++ b/.github/scripts/ai-provider/providers/gemini.js @@ -0,0 +1,107 @@ +/** + * Google AI (Gemini) Provider + * Uses Google AI API key for authentication + */ +const BaseAIProvider = require('./base-provider'); + +/** + * Gemini Provider Class + * Default model: gemini-2.5-flash-lite + * This model is a lightweight model that is designed to be fast and efficient. + * Refer to https://ai.google.dev/gemini-api/docs for the list of models. + */ +class GeminiProvider extends BaseAIProvider { + /** + * Constructor + * @param {object} config - Configuration object + * @param {string} config.model - Model to use + * @param {number} config.maxTokens - Maximum number of tokens to generate + * @param {number} config.maxRequestsPerMinute - Maximum number of requests per minute + * @param {number} config.maxTokensPerMinute - Maximum number of tokens per minute + * @param {string} apiKey - Google AI API key (required) + */ + constructor(config, apiKey) { + const model = config.model || 'gemini-2.5-flash-lite'; + super(`Google AI (${model})`, config, apiKey); + this.model = model; + } + + buildRequestOptions() { + return { + hostname: 'generativelanguage.googleapis.com', + port: 443, + path: `/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Compose-CI/1.0', + }, + }; + } + + buildRequestBody(systemPrompt, userPrompt, maxTokens) { + // Gemini combines system and user prompts + const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; + + return JSON.stringify({ + contents: [{ + parts: [{ text: combinedPrompt }] + }], + generationConfig: { + maxOutputTokens: maxTokens || this.getMaxTokens(), + temperature: 0.7, + topP: 0.95, + topK: 40, + }, + safetySettings: [ + { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }, + { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" } + ] + }); + } + + extractContent(response) { + const text = response.candidates?.[0]?.content?.parts?.[0]?.text; + if (text) { + return { + content: text, + tokens: response.usageMetadata?.totalTokenCount || null, + }; + } + return null; + } + + getRateLimits() { + return { + maxRequestsPerMinute: 15, + maxTokensPerMinute: 1000000, // 1M tokens per minute + }; + } + + +} + +/** + * Create Gemini provider + */ +function createGeminiProvider(customModel) { + const apiKey = process.env.GOOGLE_AI_API_KEY; + + + const config = { + model: customModel, + maxTokens: 2500, + maxRequestsPerMinute: 15, + maxTokensPerMinute: 1000000, + }; + + return new GeminiProvider(config, apiKey); +} + +module.exports = { + GeminiProvider, + createGeminiProvider, +}; + diff --git a/.github/scripts/ai-provider/providers/github-models.js b/.github/scripts/ai-provider/providers/github-models.js new file mode 100644 index 00000000..6d9426cc --- /dev/null +++ b/.github/scripts/ai-provider/providers/github-models.js @@ -0,0 +1,79 @@ +/** + * GitHub Models (Azure OpenAI) Provider + * Uses GitHub token for authentication in GitHub Actions + */ +const BaseAIProvider = require('./base-provider'); + +class GitHubModelsProvider extends BaseAIProvider { + constructor(config, apiKey) { + const model = config.model || 'gpt-4o'; + super(`GitHub Models (${model})`, config, apiKey); + this.model = model; + } + + buildRequestOptions() { + return { + hostname: 'models.inference.ai.azure.com', + port: 443, + path: '/chat/completions', + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'Compose-CI/1.0', + }, + }; + } + + buildRequestBody(systemPrompt, userPrompt, maxTokens) { + return JSON.stringify({ + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + model: this.model, + max_tokens: maxTokens || this.getMaxTokens(), + temperature: 0.7, + }); + } + + extractContent(response) { + if (response.choices?.[0]?.message?.content) { + return { + content: response.choices[0].message.content, + tokens: response.usage?.total_tokens || null, + }; + } + return null; + } + + getRateLimits() { + return { + maxRequestsPerMinute: 10, + maxTokensPerMinute: 40000, + }; + } +} + +/** + * Create GitHub Models provider + */ +function createGitHubProvider(customModel) { + const apiKey = process.env.GITHUB_TOKEN; + + const config = { + model: customModel, + maxTokens: 2500, + maxRequestsPerMinute: 10, + maxTokensPerMinute: 40000, + }; + + return new GitHubModelsProvider(config, apiKey); +} + +module.exports = { + GitHubModelsProvider, + createGitHubProvider, +}; + diff --git a/.github/scripts/ai-provider/rate-limiter.js b/.github/scripts/ai-provider/rate-limiter.js new file mode 100644 index 00000000..410490c4 --- /dev/null +++ b/.github/scripts/ai-provider/rate-limiter.js @@ -0,0 +1,141 @@ +/** + * Rate Limiter + * Handles request-based and token-based rate limiting + */ + +class RateLimiter { + constructor() { + this.provider = null; + this.lastCallTime = 0; + this.tokenHistory = []; + this.limits = { + maxRequestsPerMinute: 10, + maxTokensPerMinute: 40000, + }; + this.tokenWindowMs = 60000; // 60 seconds + this.safetyMargin = 0.85; // Use 85% of token budget + } + + /** + * Set the active provider and update rate limits + */ + setProvider(provider) { + this.provider = provider; + this.limits = provider.getRateLimits(); + } + + /** + * Estimate token usage for a request + * Uses rough heuristic: ~4 characters per token + */ + estimateTokenUsage(systemPrompt, userPrompt, maxTokens) { + const inputText = (systemPrompt || '') + (userPrompt || ''); + const estimatedInputTokens = Math.ceil(inputText.length / 4); + return estimatedInputTokens + (maxTokens || 0); + } + + /** + * Wait for rate limits before making a request + */ + async waitForRateLimit(estimatedTokens) { + const now = Date.now(); + + // 1. Request-based rate limit (requests per minute) + const minDelayMs = Math.ceil(60000 / this.limits.maxRequestsPerMinute); + const elapsed = now - this.lastCallTime; + + if (this.lastCallTime > 0 && elapsed < minDelayMs) { + const waitTime = minDelayMs - elapsed; + console.log(` ⏳ Rate limit: waiting ${Math.ceil(waitTime / 1000)}s...`); + await this._sleep(waitTime); + } + + // 2. Token-based rate limit + this._cleanTokenHistory(); + const currentConsumption = this._getCurrentTokenConsumption(); + const effectiveBudget = this.limits.maxTokensPerMinute * this.safetyMargin; + const availableTokens = effectiveBudget - currentConsumption; + + if (estimatedTokens > availableTokens) { + const waitTime = this._calculateTokenWaitTime(estimatedTokens, currentConsumption); + if (waitTime > 0) { + console.log( + `Waiting ${Math.ceil(waitTime / 1000)}s...` + ); + await this._sleep(waitTime); + this._cleanTokenHistory(); + } + } + + this.lastCallTime = Date.now(); + } + + /** + * Record actual token consumption after a request + */ + recordTokenConsumption(tokens) { + this.tokenHistory.push({ + timestamp: Date.now(), + tokens: tokens, + }); + this._cleanTokenHistory(); + } + + /** + * Clean expired entries from token history + */ + _cleanTokenHistory() { + const now = Date.now(); + this.tokenHistory = this.tokenHistory.filter( + entry => (now - entry.timestamp) < this.tokenWindowMs + ); + } + + /** + * Get current token consumption in the rolling window + */ + _getCurrentTokenConsumption() { + return this.tokenHistory.reduce((sum, entry) => sum + entry.tokens, 0); + } + + /** + * Calculate how long to wait for token budget to free up + */ + _calculateTokenWaitTime(tokensNeeded, currentConsumption) { + const effectiveBudget = this.limits.maxTokensPerMinute * this.safetyMargin; + const availableTokens = effectiveBudget - currentConsumption; + + if (tokensNeeded <= availableTokens) { + return 0; + } + + if (this.tokenHistory.length === 0) { + return 0; + } + + // Find how many tokens need to expire + const tokensToFree = tokensNeeded - availableTokens; + let freedTokens = 0; + let oldestTimestamp = Date.now(); + + for (const entry of this.tokenHistory) { + freedTokens += entry.tokens; + oldestTimestamp = entry.timestamp; + + if (freedTokens >= tokensToFree) { + break; + } + } + + // Calculate wait time until that entry expires + const timeUntilExpiry = this.tokenWindowMs - (Date.now() - oldestTimestamp); + return Math.max(0, timeUntilExpiry + 2000); // Add 2s buffer + } + + async _sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +module.exports = RateLimiter; + diff --git a/.github/scripts/check-solidity-comments.sh b/.github/scripts/check-solidity-comments.sh old mode 100755 new mode 100644 diff --git a/.github/scripts/generate-docs-utils/ai-enhancement.js b/.github/scripts/generate-docs-utils/ai-enhancement.js new file mode 100644 index 00000000..687072eb --- /dev/null +++ b/.github/scripts/generate-docs-utils/ai-enhancement.js @@ -0,0 +1,416 @@ +/** + * AI-powered documentation enhancement + * Uses the ai-provider service for multi-provider support + */ + +const fs = require('fs'); +const path = require('path'); +const ai = require('../ai-provider'); + +const AI_PROMPT_PATH = path.join(__dirname, '../../docs-gen-prompts.md'); +const REPO_INSTRUCTIONS_PATH = path.join(__dirname, '../../copilot-instructions.md'); + +// Load repository instructions for context +let REPO_INSTRUCTIONS = ''; +try { + REPO_INSTRUCTIONS = fs.readFileSync(REPO_INSTRUCTIONS_PATH, 'utf8'); +} catch (e) { + console.warn('Could not load copilot-instructions.md:', e.message); +} + +// Load AI prompts from markdown file +let AI_PROMPTS = { + systemPrompt: '', + modulePrompt: '', + facetPrompt: '', + relevantSections: [], + moduleFallback: { integrationNotes: '', keyFeatures: '' }, + facetFallback: { keyFeatures: '' }, +}; +try { + const promptsContent = fs.readFileSync(AI_PROMPT_PATH, 'utf8'); + AI_PROMPTS = parsePromptsFile(promptsContent); +} catch (e) { + console.warn('Could not load ai-prompts.md:', e.message); +} + +/** + * Parse the prompts markdown file to extract individual prompts + * @param {string} content - Raw markdown content + * @returns {object} Parsed prompts and configurations + */ +function parsePromptsFile(content) { + const sections = content.split(/^---$/m).map(s => s.trim()).filter(Boolean); + + const prompts = { + systemPrompt: '', + modulePrompt: '', + facetPrompt: '', + relevantSections: [], + moduleFallback: { integrationNotes: '', keyFeatures: '' }, + facetFallback: { keyFeatures: '' }, + }; + + for (const section of sections) { + if (section.includes('## System Prompt')) { + const match = section.match(/## System Prompt\s*\n([\s\S]*)/); + if (match) { + prompts.systemPrompt = match[1].trim(); + } + } else if (section.includes('## Relevant Guideline Sections')) { + // Extract sections from the code block + const codeMatch = section.match(/```\n([\s\S]*?)```/); + if (codeMatch) { + prompts.relevantSections = codeMatch[1] + .split('\n') + .map(s => s.trim()) + .filter(s => s.startsWith('## ')); + } + } else if (section.includes('## Module Prompt Template')) { + const match = section.match(/## Module Prompt Template\s*\n([\s\S]*)/); + if (match) { + prompts.modulePrompt = match[1].trim(); + } + } else if (section.includes('## Facet Prompt Template')) { + const match = section.match(/## Facet Prompt Template\s*\n([\s\S]*)/); + if (match) { + prompts.facetPrompt = match[1].trim(); + } + } else if (section.includes('## Module Fallback Content')) { + // Parse subsections for integrationNotes and keyFeatures + const integrationMatch = section.match(/### integrationNotes\s*\n([\s\S]*?)(?=###|$)/); + if (integrationMatch) { + prompts.moduleFallback.integrationNotes = integrationMatch[1].trim(); + } + const keyFeaturesMatch = section.match(/### keyFeatures\s*\n([\s\S]*?)(?=###|$)/); + if (keyFeaturesMatch) { + prompts.moduleFallback.keyFeatures = keyFeaturesMatch[1].trim(); + } + } else if (section.includes('## Facet Fallback Content')) { + const keyFeaturesMatch = section.match(/### keyFeatures\s*\n([\s\S]*?)(?=###|$)/); + if (keyFeaturesMatch) { + prompts.facetFallback.keyFeatures = keyFeaturesMatch[1].trim(); + } + } + } + + return prompts; +} + +/** + * Build the system prompt with repository context + * Uses the system prompt from the prompts file, or a fallback if not found + * @returns {string} System prompt for Copilot + */ +function buildSystemPrompt() { + let systemPrompt = AI_PROMPTS.systemPrompt || `You are a Solidity smart contract documentation expert for the Compose framework. +Always respond with valid JSON only, no markdown formatting. +Follow the project conventions and style guidelines strictly.`; + + if (REPO_INSTRUCTIONS) { + const relevantSections = AI_PROMPTS.relevantSections.length > 0 + ? AI_PROMPTS.relevantSections + : [ + '## 3. Core Philosophy', + '## 4. Facet Design Principles', + '## 5. Banned Solidity Features', + '## 6. Composability Guidelines', + '## 11. Code Style Guide', + ]; + + let contextSnippets = []; + for (const section of relevantSections) { + const startIdx = REPO_INSTRUCTIONS.indexOf(section); + if (startIdx !== -1) { + // Extract section content (up to next ## or 2000 chars max) + const nextSection = REPO_INSTRUCTIONS.indexOf('\n## ', startIdx + section.length); + const endIdx = nextSection !== -1 ? nextSection : startIdx + 2000; + const snippet = REPO_INSTRUCTIONS.slice(startIdx, Math.min(endIdx, startIdx + 2000)); + contextSnippets.push(snippet.trim()); + } + } + + if (contextSnippets.length > 0) { + systemPrompt += `\n\n--- PROJECT GUIDELINES ---\n${contextSnippets.join('\n\n')}`; + } + } + + return systemPrompt; +} + +/** + * Build the prompt for Copilot based on contract type + * @param {object} data - Parsed documentation data + * @param {'module' | 'facet'} contractType - Type of contract + * @returns {string} Prompt for Copilot + */ +function buildPrompt(data, contractType) { + const functionNames = data.functions.map(f => f.name).join(', '); + const functionDescriptions = data.functions + .map(f => `- ${f.name}: ${f.description || 'No description'}`) + .join('\n'); + + // Include events and errors for richer context + const eventNames = (data.events || []).map(e => e.name).join(', '); + const errorNames = (data.errors || []).map(e => e.name).join(', '); + + const promptTemplate = contractType === 'module' + ? AI_PROMPTS.modulePrompt + : AI_PROMPTS.facetPrompt; + + // If we have a template from the file, use it with variable substitution + if (promptTemplate) { + return promptTemplate + .replace(/\{\{title\}\}/g, data.title) + .replace(/\{\{description\}\}/g, data.description || 'No description provided') + .replace(/\{\{functionNames\}\}/g, functionNames || 'None') + .replace(/\{\{functionDescriptions\}\}/g, functionDescriptions || ' None') + .replace(/\{\{eventNames\}\}/g, eventNames || 'None') + .replace(/\{\{errorNames\}\}/g, errorNames || 'None'); + } + + // Fallback to hardcoded prompt if template not loaded + return `Given this ${contractType} documentation from the Compose diamond proxy framework, enhance it by generating: + +1. **description**: A concise one-line description (max 100 chars) for the page subtitle. Derive this from the contract's purpose based on its functions, events, and errors. + +2. **overview**: A clear, concise overview (2-3 sentences) explaining what this ${contractType} does and why it's useful in the context of diamond contracts. + +3. **usageExample**: A practical Solidity code example (10-20 lines) showing how to use this ${contractType}. For modules, show importing and calling functions. For facets, show how it would be used in a diamond. + +4. **bestPractices**: 2-3 bullet points of best practices for using this ${contractType}. + +${contractType === 'module' ? '5. **integrationNotes**: A note about how this module works with diamond storage pattern and how changes made through it are visible to facets.' : ''} + +${contractType === 'facet' ? '5. **securityConsiderations**: Important security considerations when using this facet (access control, reentrancy, etc.).' : ''} + +6. **keyFeatures**: A brief bullet list of key features. + +Contract Information: +- Name: ${data.title} +- Current Description: ${data.description || 'No description provided'} +- Functions: ${functionNames || 'None'} +- Events: ${eventNames || 'None'} +- Errors: ${errorNames || 'None'} +- Function Details: +${functionDescriptions || ' None'} + +Respond ONLY with valid JSON in this exact format (no markdown code blocks, no extra text): +{ + "description": "concise one-line description here", + "overview": "enhanced overview text here", + "usageExample": "solidity code here (use \\n for newlines)", + "bestPractices": "- Point 1\\n- Point 2\\n- Point 3", + "keyFeatures": "- Feature 1\\n- Feature 2", + ${contractType === 'module' ? '"integrationNotes": "integration notes here"' : '"securityConsiderations": "security notes here"'} +}`; +} + +/** + * Convert enhanced data fields (newlines, HTML entities) + * @param {object} enhanced - Parsed JSON from API + * @param {object} data - Original documentation data + * @returns {object} Enhanced data with converted fields + */ +function convertEnhancedFields(enhanced, data) { + // Convert literal \n strings to actual newlines + const convertNewlines = (str) => { + if (!str || typeof str !== 'string') return str; + return str.replace(/\\n/g, '\n'); + }; + + // Decode HTML entities (for code blocks) + const decodeHtmlEntities = (str) => { + if (!str || typeof str !== 'string') return str; + return str + .replace(/"/g, '"') + .replace(/=/g, '=') + .replace(/=>/g, '=>') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, "'") + .replace(/&/g, '&'); + }; + + // Use AI-generated description if provided, otherwise keep original + const aiDescription = enhanced.description?.trim(); + const finalDescription = aiDescription || data.description; + + return { + ...data, + // Description is used for page subtitle - AI improves it from NatSpec + description: finalDescription, + subtitle: finalDescription, + overview: convertNewlines(enhanced.overview) || data.overview, + usageExample: decodeHtmlEntities(convertNewlines(enhanced.usageExample)) || null, + bestPractices: convertNewlines(enhanced.bestPractices) || null, + keyFeatures: convertNewlines(enhanced.keyFeatures) || null, + integrationNotes: convertNewlines(enhanced.integrationNotes) || null, + securityConsiderations: convertNewlines(enhanced.securityConsiderations) || null, + }; +} + +/** + * Extract and clean JSON from API response + * Handles markdown code blocks, wrapped text, and attempts to fix truncated JSON + * Also removes control characters that break JSON parsing + * @param {string} content - Raw API response content + * @returns {string} Cleaned JSON string ready for parsing + */ +function extractJSON(content) { + if (!content || typeof content !== 'string') { + return content; + } + + let cleaned = content.trim(); + + // Remove markdown code blocks (```json ... ``` or ``` ... ```) + // Handle both at start and anywhere in the string + cleaned = cleaned.replace(/^```(?:json)?\s*\n?/gm, ''); + cleaned = cleaned.replace(/\n?```\s*$/gm, ''); + cleaned = cleaned.trim(); + + // Remove control characters (0x00-0x1F except newline, tab, carriage return) + // These are illegal in JSON strings and cause "Bad control character" parsing errors + cleaned = cleaned.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); + + // Find the first { and last } to extract JSON object + const firstBrace = cleaned.indexOf('{'); + const lastBrace = cleaned.lastIndexOf('}'); + + if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { + cleaned = cleaned.substring(firstBrace, lastBrace + 1); + } else if (firstBrace !== -1) { + // We have a { but no closing }, JSON might be truncated + cleaned = cleaned.substring(firstBrace); + } + + // Try to fix common truncation issues + const openBraces = (cleaned.match(/\{/g) || []).length; + const closeBraces = (cleaned.match(/\}/g) || []).length; + + if (openBraces > closeBraces) { + // JSON might be truncated - try to close incomplete strings and objects + // Check if we're in the middle of a string (simple heuristic) + const lastChar = cleaned[cleaned.length - 1]; + const lastQuote = cleaned.lastIndexOf('"'); + const lastBraceInCleaned = cleaned.lastIndexOf('}'); + + // If last quote is after last brace and not escaped, we might be in a string + if (lastQuote > lastBraceInCleaned && lastChar !== '"') { + // Check if the quote before last is escaped + let isEscaped = false; + for (let i = lastQuote - 1; i >= 0 && cleaned[i] === '\\'; i--) { + isEscaped = !isEscaped; + } + + if (!isEscaped) { + // We're likely in an incomplete string, close it + cleaned = cleaned + '"'; + } + } + + // Close any incomplete objects/arrays + const missingBraces = openBraces - closeBraces; + // Try to intelligently close - if we're in the middle of a property, add a value first + const trimmed = cleaned.trim(); + if (trimmed.endsWith(',') || trimmed.endsWith(':')) { + // We're in the middle of a property, add null and close + cleaned = cleaned.replace(/[,:]\s*$/, ': null'); + } + cleaned = cleaned + '\n' + '}'.repeat(missingBraces); + } + + return cleaned.trim(); +} + +/** + * Enhance documentation data using AI + * @param {object} data - Parsed documentation data + * @param {'module' | 'facet'} contractType - Type of contract + * @param {string} token - Legacy token parameter (deprecated, uses env vars now) + * @returns {Promise} Enhanced data + */ +async function enhanceWithAI(data, contractType, token) { + try { + console.log(` AI Content Enhancement: ${data.title}`); + + const systemPrompt = buildSystemPrompt(); + const userPrompt = buildPrompt(data, contractType); + + // Call AI provider + const responseText = await ai.call(systemPrompt, userPrompt, { + onSuccess: (text, tokens) => { + console.log(` ✅ AI enhancement complete (${tokens} tokens)`); + }, + onError: (error) => { + console.log(` ⚠️ AI call failed: ${error.message}`); + } + }); + + // Parse JSON response + let enhanced; + try { + enhanced = JSON.parse(responseText); + } catch (directParseError) { + const cleanedContent = extractJSON(responseText); + enhanced = JSON.parse(cleanedContent); + } + + return convertEnhancedFields(enhanced, data); + + } catch (error) { + console.log(` ⚠️ Enhancement failed for ${data.title}: ${error.message}`); + return addFallbackContent(data, contractType); + } +} + +/** + * Add fallback content when AI is unavailable + * @param {object} data - Documentation data + * @param {'module' | 'facet'} contractType - Type of contract + * @returns {object} Data with fallback content + */ +function addFallbackContent(data, contractType) { + console.log(' Using fallback content'); + + const enhanced = { ...data } + + if (contractType === 'module') { + enhanced.integrationNotes = AI_PROMPTS.moduleFallback.integrationNotes || + `This module accesses shared diamond storage, so changes made through this module are immediately visible to facets using the same storage pattern. All functions are internal as per Compose conventions.`; + enhanced.keyFeatures = AI_PROMPTS.moduleFallback.keyFeatures || + `- All functions are \`internal\` for use in custom facets\n- Follows diamond storage pattern (EIP-8042)\n- Compatible with ERC-2535 diamonds\n- No external dependencies or \`using\` directives`; + } else { + enhanced.keyFeatures = AI_PROMPTS.facetFallback.keyFeatures || + `- Self-contained facet with no imports or inheritance\n- Only \`external\` and \`internal\` function visibility\n- Follows Compose readability-first conventions\n- Ready for diamond integration`; + } + + return enhanced; +} + +/** + * Check if enhancement should be skipped for a file + * @param {object} data - Documentation data + * @returns {boolean} True if should skip + */ +function shouldSkipEnhancement(data) { + if (!data.functions || data.functions.length === 0) { + return true; + } + + if (data.title.startsWith('I') && data.title.length > 1 && + data.title[1] === data.title[1].toUpperCase()) { + return true; + } + + return false; +} + +module.exports = { + enhanceWithAI, + addFallbackContent, + shouldSkipEnhancement, +}; + + diff --git a/.github/scripts/generate-docs-utils/category-generator.js b/.github/scripts/generate-docs-utils/category-generator.js new file mode 100644 index 00000000..39028181 --- /dev/null +++ b/.github/scripts/generate-docs-utils/category-generator.js @@ -0,0 +1,624 @@ +/** + * Category Generator + * + * Automatically generates _category_.json files to mirror + * the src/ folder structure in the documentation. + * + * This module provides: + * - Source structure scanning + * - Category file generation + * - Path computation for doc output + * - Structure synchronization + */ + +const fs = require('fs'); +const path = require('path'); +const CONFIG = require('./config'); +const { + getCategoryItems, + createCategoryIndexFile: createIndexFile, +} = require('./category/index-page-generator'); + +// ============================================================================ +// Constants +// ============================================================================ + +/** + * Human-readable labels for directory names + * Add new entries here when adding new top-level categories + */ +const CATEGORY_LABELS = { + // Top-level categories + access: 'Access Control', + token: 'Token Standards', + diamond: 'Diamond Core', + libraries: 'Utilities', + utils: 'Utilities', + interfaceDetection: 'Interface Detection', + + // Token subcategories + ERC20: 'ERC-20', + ERC721: 'ERC-721', + ERC1155: 'ERC-1155', + ERC6909: 'ERC-6909', + Royalty: 'Royalty', + + // Access subcategories + AccessControl: 'Access Control', + AccessControlPausable: 'Pausable Access Control', + AccessControlTemporal: 'Temporal Access Control', + Owner: 'Owner', + OwnerTwoSteps: 'Two-Step Owner', +}; + +/** + * Descriptions for categories + * Add new entries here for custom descriptions + */ +const CATEGORY_DESCRIPTIONS = { + // Top-level categories + access: 'Access control patterns for permission management in Compose diamonds.', + token: 'Token standard implementations for Compose diamonds.', + diamond: 'Core diamond proxy functionality for ERC-2535 diamonds.', + libraries: 'Utility libraries and helpers for diamond development.', + utils: 'Utility libraries and helpers for diamond development.', + interfaceDetection: 'ERC-165 interface detection support.', + + // Token subcategories + ERC20: 'ERC-20 fungible token implementations.', + ERC721: 'ERC-721 non-fungible token implementations.', + ERC1155: 'ERC-1155 multi-token implementations.', + ERC6909: 'ERC-6909 minimal multi-token implementations.', + Royalty: 'ERC-2981 royalty standard implementations.', + + // Access subcategories + AccessControl: 'Role-based access control (RBAC) pattern.', + AccessControlPausable: 'RBAC with pause functionality.', + AccessControlTemporal: 'Time-limited role-based access control.', + Owner: 'Single-owner access control pattern.', + OwnerTwoSteps: 'Two-step ownership transfer pattern.', +}; + +/** + * Sidebar positions for categories + * Lower numbers appear first in the sidebar + */ +const CATEGORY_POSITIONS = { + // Top-level (lower = higher priority) + diamond: 1, + access: 2, + token: 3, + libraries: 4, + utils: 4, + interfaceDetection: 5, + + // Token subcategories + ERC20: 1, + ERC721: 2, + ERC1155: 3, + ERC6909: 4, + Royalty: 5, + + // Access subcategories + Owner: 1, + OwnerTwoSteps: 2, + AccessControl: 3, + AccessControlPausable: 4, + AccessControlTemporal: 5, + + // Leaf directories (ERC20/ERC20, etc.) - alphabetical + ERC20Bridgeable: 2, + ERC20Permit: 3, + ERC721Enumerable: 2, +}; + +// ============================================================================ +// Label & Description Generation +// ============================================================================ + +/** + * Generate a human-readable label from a directory name + * @param {string} name - Directory name (e.g., 'AccessControlPausable', 'ERC20') + * @returns {string} Human-readable label + */ +function generateLabel(name) { + // Check explicit mapping first + if (CATEGORY_LABELS[name]) { + return CATEGORY_LABELS[name]; + } + + // Handle ERC standards specially + if (/^ERC\d+/.test(name)) { + const match = name.match(/^(ERC)(\d+)(.*)$/); + if (match) { + const variant = match[3] + ? ' ' + match[3].replace(/([A-Z])/g, ' $1').trim() + : ''; + return `ERC-${match[2]}${variant}`; + } + return name; + } + + // CamelCase to Title Case with spaces + return name.replace(/([A-Z])/g, ' $1').replace(/^ /, '').trim(); +} + +/** + * Generate description for a category based on its path + * @param {string} name - Directory name + * @param {string[]} parentPath - Parent path segments + * @returns {string} Category description + */ +function generateDescription(name, parentPath = []) { + // Check explicit mapping first + if (CATEGORY_DESCRIPTIONS[name]) { + return CATEGORY_DESCRIPTIONS[name]; + } + + // Generate from context + const label = generateLabel(name); + const parent = parentPath[parentPath.length - 1]; + + if (parent === 'token') { + return `${label} token implementations with modules and facets.`; + } + if (parent === 'access') { + return `${label} access control pattern for Compose diamonds.`; + } + if (parent === 'ERC20' || parent === 'ERC721') { + return `${label} extension for ${generateLabel(parent)} tokens.`; + } + + return `${label} components for Compose diamonds.`; +} + +/** + * Get sidebar position for a category + * @param {string} name - Directory name + * @param {number} depth - Nesting depth + * @returns {number} Sidebar position + */ +function getCategoryPosition(name, depth) { + if (CATEGORY_POSITIONS[name] !== undefined) { + return CATEGORY_POSITIONS[name]; + } + return 99; // Default to end +} + +// ============================================================================ +// Source Structure Scanning +// ============================================================================ + +/** + * Check if a directory contains .sol files (directly or in subdirectories) + * @param {string} dirPath - Directory path to check + * @returns {boolean} True if contains .sol files + */ +function containsSolFiles(dirPath) { + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.sol')) { + return true; + } + if (entry.isDirectory() && !entry.name.startsWith('.')) { + if (containsSolFiles(path.join(dirPath, entry.name))) { + return true; + } + } + } + } catch (error) { + console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`); + } + + return false; +} + +/** + * Scan the src/ directory and build structure map + * @returns {Map} Map of relative paths to category info + */ +function scanSourceStructure() { + const srcDir = CONFIG.srcDir || 'src'; + const structure = new Map(); + + function scanDir(dirPath, relativePath = '') { + let entries; + try { + entries = fs.readdirSync(dirPath, { withFileTypes: true }); + } catch (error) { + console.error(`Error reading directory ${dirPath}: ${error.message}`); + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + // Skip hidden directories and interfaces + if (entry.name.startsWith('.') || entry.name === 'interfaces') { + continue; + } + + const fullPath = path.join(dirPath, entry.name); + const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + + // Only include directories that contain .sol files + if (containsSolFiles(fullPath)) { + const parts = relPath.split('/'); + structure.set(relPath, { + name: entry.name, + path: relPath, + depth: parts.length, + parent: relativePath || null, + parentParts: relativePath ? relativePath.split('/') : [], + }); + + // Recurse into subdirectories + scanDir(fullPath, relPath); + } + } + } + + if (fs.existsSync(srcDir)) { + scanDir(srcDir); + } else { + console.warn(`Warning: Source directory ${srcDir} does not exist`); + } + + return structure; +} + +// ============================================================================ +// Category File Generation +// ============================================================================ + +/** + * Map source directory name to docs directory name + * @param {string} srcName - Source directory name + * @returns {string} Documentation directory name + */ +function mapDirectoryName(srcName) { + // Map libraries -> utils for URL consistency + if (srcName === 'libraries') { + return 'utils'; + } + return srcName; +} + +/** + * Compute slug from output directory path + * @param {string} outputDir - Full output directory path + * @param {string} libraryDir - Base library directory + * @returns {string} Slug path (e.g., '/docs/library/access') + */ +function computeSlug(outputDir, libraryDir) { + const relativePath = path.relative(libraryDir, outputDir); + + if (!relativePath || relativePath.startsWith('..')) { + // Root library directory + return '/docs/library'; + } + + // Convert path separators and create slug + const normalizedPath = relativePath.replace(/\\/g, '/'); + return `/docs/library/${normalizedPath}`; +} + +/** + * Wrapper function to create category index file using the index-page-generator utility + * @param {string} outputDir - Directory to create index file in + * @param {string} relativePath - Relative path from library dir + * @param {string} label - Category label + * @param {string} description - Category description + * @param {boolean} overwrite - Whether to overwrite existing files (default: false) + * @returns {boolean} True if file was created/updated, false if skipped + */ +function createCategoryIndexFile(outputDir, relativePath, label, description, overwrite = false) { + return createIndexFile( + outputDir, + relativePath, + label, + description, + generateLabel, + generateDescription, + overwrite + ); +} + +/** + * Create a _category_.json file for a directory + * @param {string} outputDir - Directory to create category file in + * @param {string} name - Directory name + * @param {string} relativePath - Relative path from library dir + * @param {number} depth - Nesting depth + * @returns {boolean} True if file was created, false if it already existed + */ +function createCategoryFile(outputDir, name, relativePath, depth) { + const categoryFile = path.join(outputDir, '_category_.json'); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Don't overwrite existing category files (allows manual customization) + if (fs.existsSync(categoryFile)) { + return false; + } + + // Get the actual directory name from the output path (may be mapped, e.g., utils instead of libraries) + const actualDirName = path.basename(outputDir); + const parentParts = relativePath.split('/').slice(0, -1); + // Use actual directory name for label generation (supports both original and mapped names) + const label = generateLabel(actualDirName); + const position = getCategoryPosition(actualDirName, depth); + const description = generateDescription(actualDirName, parentParts); + + // Create index.mdx file first + createCategoryIndexFile(outputDir, relativePath, label, description); + + // Create category file pointing to index.mdx + const docId = relativePath ? `library/${relativePath}/index` : 'library/index'; + + const category = { + label, + position, + collapsible: true, + collapsed: true, // Collapse all categories by default + link: { + type: 'doc', + id: docId, + }, + }; + + // Ensure directory exists + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(categoryFile, JSON.stringify(category, null, 2) + '\n'); + + return true; +} + +/** + * Ensure the base library category file exists + * @param {string} libraryDir - Path to library directory + * @returns {boolean} True if created, false if existed + */ +function ensureBaseCategory(libraryDir) { + const categoryFile = path.join(libraryDir, '_category_.json'); + + if (fs.existsSync(categoryFile)) { + return false; + } + + const label = 'Library'; + const description = 'API reference for all Compose modules and facets.'; + + // Create index.mdx for base library category + createCategoryIndexFile(libraryDir, '', label, description, false); + + const baseCategory = { + label, + position: 4, + collapsible: true, + collapsed: true, // Collapse base Library category by default + link: { + type: 'doc', + id: 'library/index', + }, + }; + + fs.mkdirSync(libraryDir, { recursive: true }); + fs.writeFileSync(categoryFile, JSON.stringify(baseCategory, null, 2) + '\n'); + + return true; +} + +// ============================================================================ +// Path Computation +// ============================================================================ + +/** + * Compute output path for a source file + * Mirrors the src/ structure in website/docs/library/ + * Applies directory name mapping (e.g., libraries -> utils) + * + * @param {string} solFilePath - Path to .sol file (e.g., 'src/access/AccessControl/AccessControlMod.sol') + * @returns {object} Output path information + */ +function computeOutputPath(solFilePath) { + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Normalize path separators + const normalizedPath = solFilePath.replace(/\\/g, '/'); + + // Remove 'src/' prefix and '.sol' extension + const relativePath = normalizedPath.replace(/^src\//, '').replace(/\.sol$/, ''); + + const parts = relativePath.split('/'); + const fileName = parts.pop(); + + // Map directory names (e.g., libraries -> utils) + const mappedParts = parts.map(part => mapDirectoryName(part)); + + const outputDir = path.join(libraryDir, ...mappedParts); + const outputFile = path.join(outputDir, `${fileName}.mdx`); + + return { + outputDir, + outputFile, + relativePath: mappedParts.join('/'), + fileName, + category: mappedParts[0] || '', + subcategory: mappedParts[1] || '', + fullRelativePath: mappedParts.join('/'), + depth: mappedParts.length, + }; +} + +/** + * Ensure all parent category files exist for a given output path + * Creates _category_.json files for each directory level + * + * @param {string} outputDir - Full output directory path + */ +function ensureCategoryFiles(outputDir) { + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Get relative path from library base + const relativePath = path.relative(libraryDir, outputDir); + + if (!relativePath || relativePath.startsWith('..')) { + return; // outputDir is not under libraryDir + } + + // Ensure base category exists + ensureBaseCategory(libraryDir); + + // Walk up the directory tree, creating category files + const parts = relativePath.split(path.sep); + let currentPath = libraryDir; + + for (let i = 0; i < parts.length; i++) { + currentPath = path.join(currentPath, parts[i]); + const segment = parts[i]; + // Use the mapped path for the relative path (already mapped in computeOutputPath) + const relPath = parts.slice(0, i + 1).join('/'); + + createCategoryFile(currentPath, segment, relPath, i + 1); + } +} + +// ============================================================================ +// Structure Synchronization +// ============================================================================ + +/** + * Regenerate index.mdx files for all categories + * @param {boolean} overwrite - Whether to overwrite existing files (default: true) + * @returns {object} Summary of regenerated categories + */ +function regenerateAllIndexFiles(overwrite = true) { + const structure = scanSourceStructure(); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + const regenerated = []; + const skipped = []; + + // Regenerate base library index + const label = 'Library'; + const description = 'API reference for all Compose modules and facets.'; + if (createCategoryIndexFile(libraryDir, '', label, description, overwrite)) { + regenerated.push('library'); + } else { + skipped.push('library'); + } + + // Regenerate index for each category + const sortedPaths = Array.from(structure.entries()).sort((a, b) => + a[0].localeCompare(b[0]) + ); + + for (const [relativePath, info] of sortedPaths) { + const pathParts = relativePath.split('/'); + const mappedPathParts = pathParts.map(part => mapDirectoryName(part)); + const mappedRelativePath = mappedPathParts.join('/'); + const outputDir = path.join(libraryDir, ...mappedPathParts); + + const actualDirName = path.basename(outputDir); + const parentParts = mappedRelativePath.split('/').slice(0, -1); + const label = generateLabel(actualDirName); + const description = generateDescription(actualDirName, parentParts); + + if (createCategoryIndexFile(outputDir, mappedRelativePath, label, description, overwrite)) { + regenerated.push(mappedRelativePath); + } else { + skipped.push(mappedRelativePath); + } + } + + return { + regenerated, + skipped, + total: structure.size + 1, // +1 for base library + }; +} + +/** + * Synchronize docs structure with src structure + * Creates any missing category directories and _category_.json files + * + * @returns {object} Summary of created categories + */ +function syncDocsStructure() { + const structure = scanSourceStructure(); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + const created = []; + const existing = []; + + // Ensure base library directory exists with category + if (ensureBaseCategory(libraryDir)) { + created.push('library'); + } else { + existing.push('library'); + } + + // Create category for each directory in the structure + // Sort by path to ensure parents are created before children + const sortedPaths = Array.from(structure.entries()).sort((a, b) => + a[0].localeCompare(b[0]) + ); + + for (const [relativePath, info] of sortedPaths) { + // Map directory names in the path (e.g., libraries -> utils) + const pathParts = relativePath.split('/'); + const mappedPathParts = pathParts.map(part => mapDirectoryName(part)); + const mappedRelativePath = mappedPathParts.join('/'); + const outputDir = path.join(libraryDir, ...mappedPathParts); + + const wasCreated = createCategoryFile( + outputDir, + info.name, + mappedRelativePath, + info.depth + ); + + if (wasCreated) { + created.push(mappedRelativePath); + } else { + existing.push(mappedRelativePath); + } + } + + return { + created, + existing, + total: structure.size, + structure, + }; +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { + // Core functions + scanSourceStructure, + syncDocsStructure, + computeOutputPath, + ensureCategoryFiles, + createCategoryIndexFile, + regenerateAllIndexFiles, + + // Utilities + generateLabel, + generateDescription, + getCategoryPosition, + containsSolFiles, + mapDirectoryName, + computeSlug, + + // For extending/customizing + CATEGORY_LABELS, + CATEGORY_DESCRIPTIONS, + CATEGORY_POSITIONS, +}; + diff --git a/.github/scripts/generate-docs-utils/category/category-generator.js b/.github/scripts/generate-docs-utils/category/category-generator.js new file mode 100644 index 00000000..977e57d0 --- /dev/null +++ b/.github/scripts/generate-docs-utils/category/category-generator.js @@ -0,0 +1,624 @@ +/** + * Category Generator + * + * Automatically generates _category_.json files to mirror + * the src/ folder structure in the documentation. + * + * This module provides: + * - Source structure scanning + * - Category file generation + * - Path computation for doc output + * - Structure synchronization + */ + +const fs = require('fs'); +const path = require('path'); +const CONFIG = require('../config'); +const { + getCategoryItems, + createCategoryIndexFile: createIndexFile, +} = require('./index-page-generator'); + +// ============================================================================ +// Constants +// ============================================================================ + +/** + * Human-readable labels for directory names + * Add new entries here when adding new top-level categories + */ +const CATEGORY_LABELS = { + // Top-level categories + access: 'Access Control', + token: 'Token Standards', + diamond: 'Diamond Core', + libraries: 'Utilities', + utils: 'Utilities', + interfaceDetection: 'Interface Detection', + + // Token subcategories + ERC20: 'ERC-20', + ERC721: 'ERC-721', + ERC1155: 'ERC-1155', + ERC6909: 'ERC-6909', + Royalty: 'Royalty', + + // Access subcategories + AccessControl: 'Access Control', + AccessControlPausable: 'Pausable Access Control', + AccessControlTemporal: 'Temporal Access Control', + Owner: 'Owner', + OwnerTwoSteps: 'Two-Step Owner', +}; + +/** + * Descriptions for categories + * Add new entries here for custom descriptions + */ +const CATEGORY_DESCRIPTIONS = { + // Top-level categories + access: 'Access control patterns for permission management in Compose diamonds.', + token: 'Token standard implementations for Compose diamonds.', + diamond: 'Core diamond proxy functionality for ERC-2535 diamonds.', + libraries: 'Utility libraries and helpers for diamond development.', + utils: 'Utility libraries and helpers for diamond development.', + interfaceDetection: 'ERC-165 interface detection support.', + + // Token subcategories + ERC20: 'ERC-20 fungible token implementations.', + ERC721: 'ERC-721 non-fungible token implementations.', + ERC1155: 'ERC-1155 multi-token implementations.', + ERC6909: 'ERC-6909 minimal multi-token implementations.', + Royalty: 'ERC-2981 royalty standard implementations.', + + // Access subcategories + AccessControl: 'Role-based access control (RBAC) pattern.', + AccessControlPausable: 'RBAC with pause functionality.', + AccessControlTemporal: 'Time-limited role-based access control.', + Owner: 'Single-owner access control pattern.', + OwnerTwoSteps: 'Two-step ownership transfer pattern.', +}; + +/** + * Sidebar positions for categories + * Lower numbers appear first in the sidebar + */ +const CATEGORY_POSITIONS = { + // Top-level (lower = higher priority) + diamond: 1, + access: 2, + token: 3, + libraries: 4, + utils: 4, + interfaceDetection: 5, + + // Token subcategories + ERC20: 1, + ERC721: 2, + ERC1155: 3, + ERC6909: 4, + Royalty: 5, + + // Access subcategories + Owner: 1, + OwnerTwoSteps: 2, + AccessControl: 3, + AccessControlPausable: 4, + AccessControlTemporal: 5, + + // Leaf directories (ERC20/ERC20, etc.) - alphabetical + ERC20Bridgeable: 2, + ERC20Permit: 3, + ERC721Enumerable: 2, +}; + +// ============================================================================ +// Label & Description Generation +// ============================================================================ + +/** + * Generate a human-readable label from a directory name + * @param {string} name - Directory name (e.g., 'AccessControlPausable', 'ERC20') + * @returns {string} Human-readable label + */ +function generateLabel(name) { + // Check explicit mapping first + if (CATEGORY_LABELS[name]) { + return CATEGORY_LABELS[name]; + } + + // Handle ERC standards specially + if (/^ERC\d+/.test(name)) { + const match = name.match(/^(ERC)(\d+)(.*)$/); + if (match) { + const variant = match[3] + ? ' ' + match[3].replace(/([A-Z])/g, ' $1').trim() + : ''; + return `ERC-${match[2]}${variant}`; + } + return name; + } + + // CamelCase to Title Case with spaces + return name.replace(/([A-Z])/g, ' $1').replace(/^ /, '').trim(); +} + +/** + * Generate description for a category based on its path + * @param {string} name - Directory name + * @param {string[]} parentPath - Parent path segments + * @returns {string} Category description + */ +function generateDescription(name, parentPath = []) { + // Check explicit mapping first + if (CATEGORY_DESCRIPTIONS[name]) { + return CATEGORY_DESCRIPTIONS[name]; + } + + // Generate from context + const label = generateLabel(name); + const parent = parentPath[parentPath.length - 1]; + + if (parent === 'token') { + return `${label} token implementations with modules and facets.`; + } + if (parent === 'access') { + return `${label} access control pattern for Compose diamonds.`; + } + if (parent === 'ERC20' || parent === 'ERC721') { + return `${label} extension for ${generateLabel(parent)} tokens.`; + } + + return `${label} components for Compose diamonds.`; +} + +/** + * Get sidebar position for a category + * @param {string} name - Directory name + * @param {number} depth - Nesting depth + * @returns {number} Sidebar position + */ +function getCategoryPosition(name, depth) { + if (CATEGORY_POSITIONS[name] !== undefined) { + return CATEGORY_POSITIONS[name]; + } + return 99; // Default to end +} + +// ============================================================================ +// Source Structure Scanning +// ============================================================================ + +/** + * Check if a directory contains .sol files (directly or in subdirectories) + * @param {string} dirPath - Directory path to check + * @returns {boolean} True if contains .sol files + */ +function containsSolFiles(dirPath) { + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.sol')) { + return true; + } + if (entry.isDirectory() && !entry.name.startsWith('.')) { + if (containsSolFiles(path.join(dirPath, entry.name))) { + return true; + } + } + } + } catch (error) { + console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`); + } + + return false; +} + +/** + * Scan the src/ directory and build structure map + * @returns {Map} Map of relative paths to category info + */ +function scanSourceStructure() { + const srcDir = CONFIG.srcDir || 'src'; + const structure = new Map(); + + function scanDir(dirPath, relativePath = '') { + let entries; + try { + entries = fs.readdirSync(dirPath, { withFileTypes: true }); + } catch (error) { + console.error(`Error reading directory ${dirPath}: ${error.message}`); + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + // Skip hidden directories and interfaces + if (entry.name.startsWith('.') || entry.name === 'interfaces') { + continue; + } + + const fullPath = path.join(dirPath, entry.name); + const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + + // Only include directories that contain .sol files + if (containsSolFiles(fullPath)) { + const parts = relPath.split('/'); + structure.set(relPath, { + name: entry.name, + path: relPath, + depth: parts.length, + parent: relativePath || null, + parentParts: relativePath ? relativePath.split('/') : [], + }); + + // Recurse into subdirectories + scanDir(fullPath, relPath); + } + } + } + + if (fs.existsSync(srcDir)) { + scanDir(srcDir); + } else { + console.warn(`Warning: Source directory ${srcDir} does not exist`); + } + + return structure; +} + +// ============================================================================ +// Category File Generation +// ============================================================================ + +/** + * Map source directory name to docs directory name + * @param {string} srcName - Source directory name + * @returns {string} Documentation directory name + */ +function mapDirectoryName(srcName) { + // Map libraries -> utils for URL consistency + if (srcName === 'libraries') { + return 'utils'; + } + return srcName; +} + +/** + * Compute slug from output directory path + * @param {string} outputDir - Full output directory path + * @param {string} libraryDir - Base library directory + * @returns {string} Slug path (e.g., '/docs/library/access') + */ +function computeSlug(outputDir, libraryDir) { + const relativePath = path.relative(libraryDir, outputDir); + + if (!relativePath || relativePath.startsWith('..')) { + // Root library directory + return '/docs/library'; + } + + // Convert path separators and create slug + const normalizedPath = relativePath.replace(/\\/g, '/'); + return `/docs/library/${normalizedPath}`; +} + +/** + * Wrapper function to create category index file using the index-page-generator utility + * @param {string} outputDir - Directory to create index file in + * @param {string} relativePath - Relative path from library dir + * @param {string} label - Category label + * @param {string} description - Category description + * @param {boolean} overwrite - Whether to overwrite existing files (default: false) + * @returns {boolean} True if file was created/updated, false if skipped + */ +function createCategoryIndexFile(outputDir, relativePath, label, description, overwrite = false) { + return createIndexFile( + outputDir, + relativePath, + label, + description, + generateLabel, + generateDescription, + overwrite + ); +} + +/** + * Create a _category_.json file for a directory + * @param {string} outputDir - Directory to create category file in + * @param {string} name - Directory name + * @param {string} relativePath - Relative path from library dir + * @param {number} depth - Nesting depth + * @returns {boolean} True if file was created, false if it already existed + */ +function createCategoryFile(outputDir, name, relativePath, depth) { + const categoryFile = path.join(outputDir, '_category_.json'); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Don't overwrite existing category files (allows manual customization) + if (fs.existsSync(categoryFile)) { + return false; + } + + // Get the actual directory name from the output path (may be mapped, e.g., utils instead of libraries) + const actualDirName = path.basename(outputDir); + const parentParts = relativePath.split('/').slice(0, -1); + // Use actual directory name for label generation (supports both original and mapped names) + const label = generateLabel(actualDirName); + const position = getCategoryPosition(actualDirName, depth); + const description = generateDescription(actualDirName, parentParts); + + // Create index.mdx file first + createCategoryIndexFile(outputDir, relativePath, label, description); + + // Create category file pointing to index.mdx + const docId = relativePath ? `library/${relativePath}/index` : 'library/index'; + + const category = { + label, + position, + collapsible: true, + collapsed: true, // Collapse all categories by default + link: { + type: 'doc', + id: docId, + }, + }; + + // Ensure directory exists + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(categoryFile, JSON.stringify(category, null, 2) + '\n'); + + return true; +} + +/** + * Ensure the base library category file exists + * @param {string} libraryDir - Path to library directory + * @returns {boolean} True if created, false if existed + */ +function ensureBaseCategory(libraryDir) { + const categoryFile = path.join(libraryDir, '_category_.json'); + + if (fs.existsSync(categoryFile)) { + return false; + } + + const label = 'Library'; + const description = 'API reference for all Compose modules and facets.'; + + // Create index.mdx for base library category + createCategoryIndexFile(libraryDir, '', label, description, false); + + const baseCategory = { + label, + position: 4, + collapsible: true, + collapsed: true, // Collapse base Library category by default + link: { + type: 'doc', + id: 'library/index', + }, + }; + + fs.mkdirSync(libraryDir, { recursive: true }); + fs.writeFileSync(categoryFile, JSON.stringify(baseCategory, null, 2) + '\n'); + + return true; +} + +// ============================================================================ +// Path Computation +// ============================================================================ + +/** + * Compute output path for a source file + * Mirrors the src/ structure in website/docs/library/ + * Applies directory name mapping (e.g., libraries -> utils) + * + * @param {string} solFilePath - Path to .sol file (e.g., 'src/access/AccessControl/AccessControlMod.sol') + * @returns {object} Output path information + */ +function computeOutputPath(solFilePath) { + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Normalize path separators + const normalizedPath = solFilePath.replace(/\\/g, '/'); + + // Remove 'src/' prefix and '.sol' extension + const relativePath = normalizedPath.replace(/^src\//, '').replace(/\.sol$/, ''); + + const parts = relativePath.split('/'); + const fileName = parts.pop(); + + // Map directory names (e.g., libraries -> utils) + const mappedParts = parts.map(part => mapDirectoryName(part)); + + const outputDir = path.join(libraryDir, ...mappedParts); + const outputFile = path.join(outputDir, `${fileName}.mdx`); + + return { + outputDir, + outputFile, + relativePath: mappedParts.join('/'), + fileName, + category: mappedParts[0] || '', + subcategory: mappedParts[1] || '', + fullRelativePath: mappedParts.join('/'), + depth: mappedParts.length, + }; +} + +/** + * Ensure all parent category files exist for a given output path + * Creates _category_.json files for each directory level + * + * @param {string} outputDir - Full output directory path + */ +function ensureCategoryFiles(outputDir) { + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + // Get relative path from library base + const relativePath = path.relative(libraryDir, outputDir); + + if (!relativePath || relativePath.startsWith('..')) { + return; // outputDir is not under libraryDir + } + + // Ensure base category exists + ensureBaseCategory(libraryDir); + + // Walk up the directory tree, creating category files + const parts = relativePath.split(path.sep); + let currentPath = libraryDir; + + for (let i = 0; i < parts.length; i++) { + currentPath = path.join(currentPath, parts[i]); + const segment = parts[i]; + // Use the mapped path for the relative path (already mapped in computeOutputPath) + const relPath = parts.slice(0, i + 1).join('/'); + + createCategoryFile(currentPath, segment, relPath, i + 1); + } +} + +// ============================================================================ +// Structure Synchronization +// ============================================================================ + +/** + * Regenerate index.mdx files for all categories + * @param {boolean} overwrite - Whether to overwrite existing files (default: true) + * @returns {object} Summary of regenerated categories + */ +function regenerateAllIndexFiles(overwrite = true) { + const structure = scanSourceStructure(); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + const regenerated = []; + const skipped = []; + + // Regenerate base library index + const label = 'Library'; + const description = 'API reference for all Compose modules and facets.'; + if (createCategoryIndexFile(libraryDir, '', label, description, overwrite)) { + regenerated.push('library'); + } else { + skipped.push('library'); + } + + // Regenerate index for each category + const sortedPaths = Array.from(structure.entries()).sort((a, b) => + a[0].localeCompare(b[0]) + ); + + for (const [relativePath, info] of sortedPaths) { + const pathParts = relativePath.split('/'); + const mappedPathParts = pathParts.map(part => mapDirectoryName(part)); + const mappedRelativePath = mappedPathParts.join('/'); + const outputDir = path.join(libraryDir, ...mappedPathParts); + + const actualDirName = path.basename(outputDir); + const parentParts = mappedRelativePath.split('/').slice(0, -1); + const label = generateLabel(actualDirName); + const description = generateDescription(actualDirName, parentParts); + + if (createCategoryIndexFile(outputDir, mappedRelativePath, label, description, overwrite)) { + regenerated.push(mappedRelativePath); + } else { + skipped.push(mappedRelativePath); + } + } + + return { + regenerated, + skipped, + total: structure.size + 1, // +1 for base library + }; +} + +/** + * Synchronize docs structure with src structure + * Creates any missing category directories and _category_.json files + * + * @returns {object} Summary of created categories + */ +function syncDocsStructure() { + const structure = scanSourceStructure(); + const libraryDir = CONFIG.libraryOutputDir || 'website/docs/library'; + + const created = []; + const existing = []; + + // Ensure base library directory exists with category + if (ensureBaseCategory(libraryDir)) { + created.push('library'); + } else { + existing.push('library'); + } + + // Create category for each directory in the structure + // Sort by path to ensure parents are created before children + const sortedPaths = Array.from(structure.entries()).sort((a, b) => + a[0].localeCompare(b[0]) + ); + + for (const [relativePath, info] of sortedPaths) { + // Map directory names in the path (e.g., libraries -> utils) + const pathParts = relativePath.split('/'); + const mappedPathParts = pathParts.map(part => mapDirectoryName(part)); + const mappedRelativePath = mappedPathParts.join('/'); + const outputDir = path.join(libraryDir, ...mappedPathParts); + + const wasCreated = createCategoryFile( + outputDir, + info.name, + mappedRelativePath, + info.depth + ); + + if (wasCreated) { + created.push(mappedRelativePath); + } else { + existing.push(mappedRelativePath); + } + } + + return { + created, + existing, + total: structure.size, + structure, + }; +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { + // Core functions + scanSourceStructure, + syncDocsStructure, + computeOutputPath, + ensureCategoryFiles, + createCategoryIndexFile, + regenerateAllIndexFiles, + + // Utilities + generateLabel, + generateDescription, + getCategoryPosition, + containsSolFiles, + mapDirectoryName, + computeSlug, + + // For extending/customizing + CATEGORY_LABELS, + CATEGORY_DESCRIPTIONS, + CATEGORY_POSITIONS, +}; + diff --git a/.github/scripts/generate-docs-utils/category/index-page-generator.js b/.github/scripts/generate-docs-utils/category/index-page-generator.js new file mode 100644 index 00000000..9b625aac --- /dev/null +++ b/.github/scripts/generate-docs-utils/category/index-page-generator.js @@ -0,0 +1,209 @@ +/** + * Index Page Generator + * + * Generates index.mdx files for category directories with custom DocCard components. + * This module provides utilities for creating styled category index pages. + */ + +const fs = require('fs'); +const path = require('path'); +const CONFIG = require('../config'); + +// ============================================================================ +// Category Items Discovery +// ============================================================================ + +/** + * Get all items (documents and subcategories) in a directory + * @param {string} outputDir - Directory to scan + * @param {string} relativePath - Relative path from library dir + * @param {Function} generateLabel - Function to generate labels from names + * @param {Function} generateDescription - Function to generate descriptions + * @returns {Array} Array of items with type, name, label, href, description + */ +function getCategoryItems(outputDir, relativePath, generateLabel, generateDescription) { + const items = []; + + if (!fs.existsSync(outputDir)) { + return items; + } + + const entries = fs.readdirSync(outputDir, { withFileTypes: true }); + + for (const entry of entries) { + // Skip hidden files, category files, and index files + if (entry.name.startsWith('.') || + entry.name === '_category_.json' || + entry.name === 'index.mdx') { + continue; + } + + if (entry.isFile() && entry.name.endsWith('.mdx')) { + // It's a document + const docName = entry.name.replace('.mdx', ''); + const docPath = path.join(outputDir, entry.name); + + // Try to read frontmatter for title and description + let title = generateLabel(docName); + let description = ''; + + try { + const content = fs.readFileSync(docPath, 'utf8'); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const titleMatch = frontmatter.match(/^title:\s*["']?(.*?)["']?$/m); + const descMatch = frontmatter.match(/^description:\s*["']?(.*?)["']?$/m); + if (titleMatch) title = titleMatch[1].trim(); + if (descMatch) description = descMatch[1].trim(); + } + } catch (error) { + // If reading fails, use defaults + } + + const docRelativePath = relativePath ? `${relativePath}/${docName}` : docName; + items.push({ + type: 'doc', + name: docName, + label: title, + description: description, + href: `/docs/library/${docRelativePath}`, + }); + } else if (entry.isDirectory()) { + // It's a subcategory + const subcategoryName = entry.name; + const subcategoryLabel = generateLabel(subcategoryName); + const subcategoryRelativePath = relativePath ? `${relativePath}/${subcategoryName}` : subcategoryName; + const subcategoryDescription = generateDescription(subcategoryName, relativePath.split('/')); + + items.push({ + type: 'category', + name: subcategoryName, + label: subcategoryLabel, + description: subcategoryDescription, + href: `/docs/library/${subcategoryRelativePath}`, + }); + } + } + + // Sort items: categories first, then docs, both alphabetically + items.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'category' ? -1 : 1; + } + return a.label.localeCompare(b.label); + }); + + return items; +} + +// ============================================================================ +// MDX Content Generation +// ============================================================================ + +/** + * Generate MDX content for a category index page + * @param {string} label - Category label + * @param {string} description - Category description + * @param {Array} items - Array of items to display + * @returns {string} Generated MDX content + */ +function generateIndexMdxContent(label, description, items) { + // Escape quotes in label and description for frontmatter + const escapedLabel = label.replace(/"/g, '\\"'); + const escapedDescription = description.replace(/"/g, '\\"'); + + let mdxContent = `--- +title: "${escapedLabel}" +description: "${escapedDescription}" +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ${escapedDescription} + + +`; + + if (items.length > 0) { + mdxContent += `\n`; + + for (const item of items) { + const iconName = item.type === 'category' ? 'package' : 'book'; + const itemDescription = item.description ? `"${item.description.replace(/"/g, '\\"')}"` : '""'; + + mdxContent += ` } + size="medium" + />\n`; + } + + mdxContent += `\n`; + } else { + mdxContent += `_No items in this category yet._\n`; + } + + return mdxContent; +} + +// ============================================================================ +// Index File Creation +// ============================================================================ + +/** + * Generate index.mdx file for a category + * @param {string} outputDir - Directory to create index file in + * @param {string} relativePath - Relative path from library dir + * @param {string} label - Category label + * @param {string} description - Category description + * @param {Function} generateLabel - Function to generate labels from names + * @param {Function} generateDescription - Function to generate descriptions + * @param {boolean} overwrite - Whether to overwrite existing files (default: false) + * @returns {boolean} True if file was created/updated, false if skipped + */ +function createCategoryIndexFile( + outputDir, + relativePath, + label, + description, + generateLabel, + generateDescription, + overwrite = false +) { + const indexFile = path.join(outputDir, 'index.mdx'); + + // Don't overwrite existing index files unless explicitly requested (allows manual customization) + if (!overwrite && fs.existsSync(indexFile)) { + return false; + } + + // Get items in this category + const items = getCategoryItems(outputDir, relativePath, generateLabel, generateDescription); + + // Generate MDX content + const mdxContent = generateIndexMdxContent(label, description, items); + + // Ensure directory exists + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(indexFile, mdxContent); + + return true; +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { + getCategoryItems, + generateIndexMdxContent, + createCategoryIndexFile, +}; + diff --git a/.github/scripts/generate-docs-utils/config.js b/.github/scripts/generate-docs-utils/config.js new file mode 100644 index 00000000..e436c41a --- /dev/null +++ b/.github/scripts/generate-docs-utils/config.js @@ -0,0 +1,153 @@ +/** + * Configuration for documentation generation + * + * Centralized configuration for paths, settings, and defaults. + * Modify this file to change documentation output paths or behavior. + */ + +module.exports = { + // ============================================================================ + // Input Paths + // ============================================================================ + + /** Directory containing forge doc output */ + forgeDocsDir: 'docs/src/src', + + /** Source code directory to mirror */ + srcDir: 'src', + + // ============================================================================ + // Output Paths + // ============================================================================ + + /** + * Base output directory for contract documentation + * Structure mirrors src/ automatically + */ + contractsOutputDir: 'website/docs/contracts', + + // ============================================================================ + // Sidebar Positions + // ============================================================================ + + /** Default sidebar position for contracts without explicit mapping */ + defaultSidebarPosition: 50, + + /** + * Contract-specific sidebar positions + * Maps contract name to position number (lower = higher in sidebar) + * + * Convention: + * - Modules come before their corresponding facets + * - Core/base contracts come before extensions + * - Burn facets come after main facets + */ + contractPositions: { + // Diamond core + DiamondMod: 1, + DiamondCutMod: 2, + DiamondCutFacet: 3, + DiamondLoupeFacet: 4, + + // Access - Owner pattern + OwnerMod: 1, + OwnerFacet: 2, + + // Access - Two-step owner + OwnerTwoStepsMod: 1, + OwnerTwoStepsFacet: 2, + + // Access - AccessControl pattern + AccessControlMod: 1, + AccessControlFacet: 2, + + // Access - AccessControlPausable + AccessControlPausableMod: 1, + AccessControlPausableFacet: 2, + + // Access - AccessControlTemporal + AccessControlTemporalMod: 1, + AccessControlTemporalFacet: 2, + + // ERC-20 base + ERC20Mod: 1, + ERC20Facet: 2, + ERC20BurnFacet: 3, + + // ERC-20 Bridgeable + ERC20BridgeableMod: 1, + ERC20BridgeableFacet: 2, + + // ERC-20 Permit + ERC20PermitMod: 1, + ERC20PermitFacet: 2, + + // ERC-721 base + ERC721Mod: 1, + ERC721Facet: 2, + ERC721BurnFacet: 3, + + // ERC-721 Enumerable + ERC721EnumerableMod: 1, + ERC721EnumerableFacet: 2, + ERC721EnumerableBurnFacet: 3, + + // ERC-1155 + ERC1155Mod: 1, + ERC1155Facet: 2, + + // ERC-6909 + ERC6909Mod: 1, + ERC6909Facet: 2, + + // Royalty + RoyaltyMod: 1, + RoyaltyFacet: 2, + + // Libraries + NonReentrancyMod: 1, + ERC165Mod: 1, + }, + + // ============================================================================ + // Repository Configuration + // ============================================================================ + + /** Main repository URL - always use this for source links */ + mainRepoUrl: 'https://github.com/Perfect-Abstractions/Compose', + + /** + * Normalize gitSource URL to always point to the main repository's main branch + * Replaces any fork or incorrect repository URLs with the main repo URL + * Converts blob URLs to tree URLs pointing to main branch + * @param {string} gitSource - Original gitSource URL from forge doc + * @returns {string} Normalized gitSource URL + */ + normalizeGitSource(gitSource) { + if (!gitSource) return gitSource; + + // Pattern: https://github.com/USER/Compose/blob/COMMIT/src/path/to/file.sol + // Convert to: https://github.com/Perfect-Abstractions/Compose/tree/main/src/path/to/file.sol + const githubUrlPattern = /https:\/\/github\.com\/[^\/]+\/Compose\/(?:blob|tree)\/[^\/]+\/(.+)/; + const match = gitSource.match(githubUrlPattern); + + if (match) { + // Extract the path after the repo name (should start with src/) + const pathPart = match[1]; + // Ensure it starts with src/ (remove any leading src/ if duplicated) + const normalizedPath = pathPart.startsWith('src/') ? pathPart : `src/${pathPart}`; + return `${this.mainRepoUrl}/tree/main/${normalizedPath}`; + } + + // If it doesn't match the pattern, try to construct from the main repo + // Extract just the file path if it's a relative path or partial URL + if (gitSource.includes('/src/')) { + const srcIndex = gitSource.indexOf('/src/'); + const pathAfterSrc = gitSource.substring(srcIndex + 1); + return `${this.mainRepoUrl}/tree/main/${pathAfterSrc}`; + } + + // If it doesn't match any pattern, return as-is (might be a different format) + return gitSource; + }, +}; diff --git a/.github/scripts/generate-docs-utils/doc-generation-utils.js b/.github/scripts/generate-docs-utils/doc-generation-utils.js new file mode 100644 index 00000000..9155ecbd --- /dev/null +++ b/.github/scripts/generate-docs-utils/doc-generation-utils.js @@ -0,0 +1,428 @@ +/** + * Documentation Generation Utilities + * + * Provides helper functions for: + * - Finding and reading Solidity source files + * - Detecting contract types (module vs facet) + * - Computing output paths (mirrors src/ structure) + * - Extracting documentation from source files + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { readFileSafe } = require('../workflow-utils'); +const CONFIG = require('./config'); +const { + computeOutputPath, + ensureCategoryFiles, +} = require('./category/category-generator'); + +// ============================================================================ +// Git Integration +// ============================================================================ + +/** + * Get list of changed Solidity files from git diff + * @param {string} baseBranch - Base branch to compare against + * @returns {string[]} Array of changed .sol file paths + */ +function getChangedSolFiles(baseBranch = 'HEAD~1') { + try { + const output = execSync(`git diff --name-only ${baseBranch} HEAD -- 'src/**/*.sol'`, { + encoding: 'utf8', + }); + return output + .trim() + .split('\n') + .filter((f) => f.endsWith('.sol')); + } catch (error) { + console.error('Error getting changed files:', error.message); + return []; + } +} + +/** + * Get all Solidity files in src directory + * @returns {string[]} Array of .sol file paths + */ +function getAllSolFiles() { + try { + const output = execSync('find src -name "*.sol" -type f', { + encoding: 'utf8', + }); + return output + .trim() + .split('\n') + .filter((f) => f); + } catch (error) { + console.error('Error getting all sol files:', error.message); + return []; + } +} + +/** + * Read changed files from a file (used in CI) + * @param {string} filePath - Path to file containing list of changed files + * @returns {string[]} Array of file paths + */ +function readChangedFilesFromFile(filePath) { + const content = readFileSafe(filePath); + if (!content) { + return []; + } + return content + .trim() + .split('\n') + .filter((f) => f.endsWith('.sol')); +} + +// ============================================================================ +// Forge Doc Integration +// ============================================================================ + +/** + * Find forge doc output files for a given source file + * @param {string} solFilePath - Path to .sol file (e.g., 'src/access/AccessControl/AccessControlMod.sol') + * @returns {string[]} Array of markdown file paths from forge doc output + */ +function findForgeDocFiles(solFilePath) { + // Transform: src/access/AccessControl/AccessControlMod.sol + // To: docs/src/src/access/AccessControl/AccessControlMod.sol/ + const relativePath = solFilePath.replace(/^src\//, ''); + const docsDir = path.join(CONFIG.forgeDocsDir, relativePath); + + if (!fs.existsSync(docsDir)) { + return []; + } + + try { + const files = fs.readdirSync(docsDir); + return files.filter((f) => f.endsWith('.md')).map((f) => path.join(docsDir, f)); + } catch (error) { + console.error(`Error reading docs dir ${docsDir}:`, error.message); + return []; + } +} + +// ============================================================================ +// Contract Type Detection +// ============================================================================ + +/** + * Determine if a contract is an interface + * Interfaces should be skipped from documentation generation + * Only checks the naming pattern (I[A-Z]) to avoid false positives + * @param {string} title - Contract title/name + * @param {string} content - File content (forge doc markdown) - unused but kept for API compatibility + * @returns {boolean} True if this is an interface + */ +function isInterface(title, content) { + // Only check if title follows interface naming convention: starts with "I" followed by uppercase + // This is the most reliable indicator and avoids false positives from content that mentions "interface" + if (title && /^I[A-Z]/.test(title)) { + return true; + } + + // Removed content-based check to avoid false positives + // Facets and contracts often mention "interface" in their descriptions + // (e.g., "ERC-165 Standard Interface Detection Facet") which would incorrectly filter them + + return false; +} + +/** + * Determine if a contract is a module or facet + * @param {string} filePath - Path to the file + * @param {string} content - File content + * @returns {'module' | 'facet'} Contract type + */ +function getContractType(filePath, content) { + const lowerPath = filePath.toLowerCase(); + const normalizedPath = lowerPath.replace(/\\/g, '/'); + const baseName = path.basename(filePath, path.extname(filePath)).toLowerCase(); + + // Explicit modules folder + if (normalizedPath.includes('/modules/')) { + return 'module'; + } + + // File naming conventions (e.g., AccessControlMod.sol, NonReentrancyModule.sol) + if (baseName.endsWith('mod') || baseName.endsWith('module')) { + return 'module'; + } + + if (lowerPath.includes('facet')) { + return 'facet'; + } + + // Libraries folder typically contains modules + if (normalizedPath.includes('/libraries/')) { + return 'module'; + } + + // Default to facet for contracts + return 'facet'; +} + +// ============================================================================ +// Output Path Computation +// ============================================================================ + +/** + * Get output directory and file path based on source file path + * Mirrors the src/ structure in website/docs/contracts/ + * + * @param {string} solFilePath - Path to the source .sol file + * @param {'module' | 'facet'} contractType - Type of contract (for logging) + * @returns {object} { outputDir, outputFile, relativePath, fileName, category } + */ +function getOutputPath(solFilePath, contractType) { + // Compute path using the new structure-mirroring logic + const pathInfo = computeOutputPath(solFilePath); + + // Ensure all parent category files exist + ensureCategoryFiles(pathInfo.outputDir); + + return pathInfo; +} + +/** + * Get sidebar position for a contract + * @param {string} contractName - Name of the contract + * @returns {number} Sidebar position + */ +function getSidebarPosition(contractName) { + if (CONFIG.contractPositions && CONFIG.contractPositions[contractName] !== undefined) { + return CONFIG.contractPositions[contractName]; + } + return CONFIG.defaultSidebarPosition || 50; +} + +// ============================================================================ +// Source File Parsing +// ============================================================================ + +/** + * Extract module name from file path + * @param {string} filePath - Path to the file + * @returns {string} Module name + */ +function extractModuleNameFromPath(filePath) { + // If it's a constants file, extract from filename + const basename = path.basename(filePath); + if (basename.startsWith('constants.')) { + const match = basename.match(/^constants\.(.+)\.md$/); + if (match) { + return match[1]; + } + } + + // Extract from .sol file path + if (filePath.endsWith('.sol')) { + return path.basename(filePath, '.sol'); + } + + // Extract from directory structure + const parts = filePath.split(path.sep); + for (let i = parts.length - 1; i >= 0; i--) { + if (parts[i].endsWith('.sol')) { + return path.basename(parts[i], '.sol'); + } + } + + // Fallback: use basename without extension + return path.basename(filePath, path.extname(filePath)); +} + +/** + * Check if a line is a code element declaration + * @param {string} line - Trimmed line to check + * @returns {boolean} True if line is a code element declaration + */ +function isCodeElementDeclaration(line) { + if (!line) return false; + return ( + line.startsWith('function ') || + line.startsWith('error ') || + line.startsWith('event ') || + line.startsWith('struct ') || + line.startsWith('enum ') || + line.startsWith('contract ') || + line.startsWith('library ') || + line.startsWith('interface ') || + line.startsWith('modifier ') || + /^\w+\s+(constant|immutable)\s/.test(line) || + /^(bytes32|uint\d*|int\d*|address|bool|string)\s+constant\s/.test(line) + ); +} + +/** + * Extract module description from source file NatSpec comments + * @param {string} solFilePath - Path to the Solidity source file + * @returns {string} Description extracted from @title and @notice tags + */ +function extractModuleDescriptionFromSource(solFilePath) { + const content = readFileSafe(solFilePath); + if (!content) { + return ''; + } + + const lines = content.split('\n'); + let inComment = false; + let commentBuffer = []; + let title = ''; + let notice = ''; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + // Skip SPDX and pragma lines + if (trimmed.startsWith('// SPDX') || trimmed.startsWith('pragma ')) { + continue; + } + + // Check if we've reached a code element without finding a file-level comment + if (!inComment && isCodeElementDeclaration(trimmed)) { + break; + } + + // Start of block comment + if (trimmed.startsWith('/**') || trimmed.startsWith('/*')) { + inComment = true; + commentBuffer = []; + continue; + } + + // End of block comment + if (inComment && trimmed.includes('*/')) { + inComment = false; + const commentText = commentBuffer.join(' '); + + // Look ahead to see if next non-empty line is a code element + let nextCodeLine = ''; + for (let j = i + 1; j < lines.length && j < i + 5; j++) { + const nextTrimmed = lines[j].trim(); + if (nextTrimmed && !nextTrimmed.startsWith('//') && !nextTrimmed.startsWith('/*')) { + nextCodeLine = nextTrimmed; + break; + } + } + + // If the comment has @title, it's a file-level comment + const titleMatch = commentText.match(/@title\s+(.+?)(?:\s+@|\s*$)/); + if (titleMatch) { + title = titleMatch[1].trim(); + const noticeMatch = commentText.match(/@notice\s+(.+?)(?:\s+@|\s*$)/); + if (noticeMatch) { + notice = noticeMatch[1].trim(); + } + break; + } + + // If next line is a code element, this comment belongs to that element + if (isCodeElementDeclaration(nextCodeLine)) { + commentBuffer = []; + continue; + } + + // Standalone comment with @notice + const standaloneNotice = commentText.match(/@notice\s+(.+?)(?:\s+@|\s*$)/); + if (standaloneNotice && !isCodeElementDeclaration(nextCodeLine)) { + notice = standaloneNotice[1].trim(); + break; + } + + commentBuffer = []; + continue; + } + + // Collect comment lines + if (inComment) { + let cleanLine = trimmed + .replace(/^\*\s*/, '') + .replace(/^\s*\*/, '') + .trim(); + if (cleanLine && !cleanLine.startsWith('*/')) { + commentBuffer.push(cleanLine); + } + } + } + + // Combine title and notice + if (title && notice) { + return `${title} - ${notice}`; + } else if (notice) { + return notice; + } else if (title) { + return title; + } + + return ''; +} + +/** + * Generate a fallback description from contract name + * + * This is a minimal, generic fallback used only when: + * 1. No NatSpec @title/@notice exists in source + * 2. AI enhancement will improve it later + * + * The AI enhancement step receives this as input and generates + * a richer, context-aware description from the actual code. + * + * @param {string} contractName - Name of the contract + * @returns {string} Generic description (will be enhanced by AI) + */ +function generateDescriptionFromName(contractName) { + if (!contractName) return ''; + + // Detect library type from naming convention + const isModule = contractName.endsWith('Mod') || contractName.endsWith('Module'); + const isFacet = contractName.endsWith('Facet'); + const typeLabel = isModule ? 'module' : isFacet ? 'facet' : 'library'; + + // Remove suffix and convert CamelCase to readable text + const baseName = contractName + .replace(/Mod$/, '') + .replace(/Module$/, '') + .replace(/Facet$/, ''); + + // Convert CamelCase to readable format + // Handles: ERC20 -> ERC-20, AccessControl -> Access Control + const readable = baseName + .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase splits + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // acronym handling + .replace(/^ERC(\d+)/, 'ERC-$1') // ERC20 -> ERC-20 + .trim(); + + return `${readable} ${typeLabel} for Compose diamonds`; +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { + // Git integration + getChangedSolFiles, + getAllSolFiles, + readChangedFilesFromFile, + + // Forge doc integration + findForgeDocFiles, + + // Contract type detection + isInterface, + getContractType, + + // Output path computation + getOutputPath, + getSidebarPosition, + + // Source file parsing + extractModuleNameFromPath, + extractModuleDescriptionFromSource, + generateDescriptionFromName, +}; diff --git a/.github/scripts/generate-docs-utils/forge-doc-parser.js b/.github/scripts/generate-docs-utils/forge-doc-parser.js new file mode 100644 index 00000000..4e0ef4d0 --- /dev/null +++ b/.github/scripts/generate-docs-utils/forge-doc-parser.js @@ -0,0 +1,750 @@ +/** + * Parser for forge doc markdown output + * Extracts structured data from forge-generated markdown files + */ + +const config = require('./config'); + +/** + * Parse forge doc markdown output into structured data + * @param {string} content - Markdown content from forge doc + * @param {string} filePath - Path to the markdown file + * @returns {object} Parsed documentation data + */ +function parseForgeDocMarkdown(content, filePath) { + const data = { + title: '', + description: '', + subtitle: '', + overview: '', + gitSource: '', + functions: [], + events: [], + errors: [], + structs: [], + stateVariables: [], + }; + + const lines = content.split('\n'); + let currentSection = null; + let currentItem = null; + let itemType = null; + let collectingDescription = false; + let descriptionBuffer = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Parse title (# heading) + if (line.startsWith('# ') && !data.title) { + data.title = line.replace('# ', '').trim(); + continue; + } + + // Parse git source link + if (trimmedLine.startsWith('[Git Source]')) { + const match = trimmedLine.match(/\[Git Source\]\((.*?)\)/); + if (match) { + data.gitSource = config.normalizeGitSource(match[1]); + } + continue; + } + + // Parse description (first non-empty lines after title, before sections) + if (data.title && !currentSection && trimmedLine && !line.startsWith('#') && !line.startsWith('[')) { + const sanitizedLine = cleanDescription(sanitizeBrokenLinks(trimmedLine)); + if (!data.description) { + data.description = sanitizedLine; + data.subtitle = sanitizedLine; + } else if (!data.overview) { + // Capture additional lines as overview + data.overview = data.description + '\n\n' + sanitizedLine; + } + continue; + } + + // Parse main sections + if (line.startsWith('## ')) { + const sectionName = line.replace('## ', '').trim().toLowerCase(); + + // Save current item before switching sections + if (currentItem) { + saveCurrentItem(data, currentItem, itemType); + currentItem = null; + itemType = null; + } + + if (sectionName === 'functions') { + currentSection = 'functions'; + } else if (sectionName === 'events') { + currentSection = 'events'; + } else if (sectionName === 'errors') { + currentSection = 'errors'; + } else if (sectionName === 'structs') { + currentSection = 'structs'; + } else if (sectionName === 'state variables') { + currentSection = 'stateVariables'; + } else { + currentSection = null; + } + continue; + } + + // Parse item definitions (### heading) + if (line.startsWith('### ') && currentSection) { + // Save previous item + if (currentItem) { + saveCurrentItem(data, currentItem, itemType); + } + + const name = line.replace('### ', '').trim(); + itemType = currentSection; + currentItem = createNewItem(name, currentSection); + collectingDescription = true; + descriptionBuffer = []; + continue; + } + + // Process content within current item + if (currentItem) { + // Code block with signature + if (line.startsWith('```solidity')) { + const codeLines = []; + i++; + while (i < lines.length && !lines[i].startsWith('```')) { + codeLines.push(lines[i]); + i++; + } + const codeContent = codeLines.join('\n').trim(); + + if (currentSection === 'functions' || currentSection === 'events' || currentSection === 'errors') { + currentItem.signature = codeContent; + + // Extract mutability from signature + if (codeContent.includes(' view ')) { + currentItem.mutability = 'view'; + } else if (codeContent.includes(' pure ')) { + currentItem.mutability = 'pure'; + } else if (codeContent.includes(' payable ')) { + currentItem.mutability = 'payable'; + } + } else if (currentSection === 'structs') { + currentItem.definition = codeContent; + } else if (currentSection === 'stateVariables') { + // Extract type and value from constant definition + // Format: "bytes32 constant NAME = value;" or "bytes32 NAME = value;" + // Handle both with and without "constant" keyword + // Note: name is already known from the ### heading, so we just need type and value + const constantMatch = codeContent.match(/(\w+(?:\s*\d+)?)\s+(?:constant\s+)?\w+\s*=\s*(.+?)(?:\s*;)?$/); + if (constantMatch) { + currentItem.type = constantMatch[1]; + currentItem.value = constantMatch[2].trim(); + } else { + // Fallback: try to extract just the value part if it's a simple assignment + const simpleMatch = codeContent.match(/=\s*(.+?)(?:\s*;)?$/); + if (simpleMatch) { + currentItem.value = simpleMatch[1].trim(); + } + // Try to extract type from the beginning + const typeMatch = codeContent.match(/^(\w+(?:\s*\d+)?)\s+/); + if (typeMatch) { + currentItem.type = typeMatch[1]; + } + } + } + continue; + } + + // Description text (before **Parameters** or **Returns**) + if (collectingDescription && trimmedLine && !trimmedLine.startsWith('**') && !trimmedLine.startsWith('|')) { + descriptionBuffer.push(trimmedLine); + continue; + } + + // End description collection on special markers + if (trimmedLine.startsWith('**Parameters**') || trimmedLine.startsWith('**Returns**')) { + if (descriptionBuffer.length > 0) { + const description = cleanDescription(sanitizeBrokenLinks(descriptionBuffer.join(' ').trim())); + currentItem.description = description; + currentItem.notice = description; + descriptionBuffer = []; + } + collectingDescription = false; + } + + // Parse table rows (Parameters or Returns) + if (trimmedLine.startsWith('|') && !trimmedLine.includes('----')) { + const cells = trimmedLine.split('|').map(c => c.trim()).filter(c => c); + + // Skip header row + if (cells.length >= 3 && cells[0] !== 'Name' && cells[0] !== 'Parameter') { + const paramName = cells[0].replace(/`/g, '').trim(); + const paramType = cells[1].replace(/`/g, '').trim(); + const paramDesc = sanitizeBrokenLinks(cells[2] || ''); + + // Skip if parameter name matches the function name (parsing error) + if (currentItem && paramName === currentItem.name) { + continue; + } + + // Determine if Parameters or Returns based on preceding lines + const precedingLines = lines.slice(Math.max(0, i - 10), i).join('\n'); + + if (precedingLines.includes('**Returns**')) { + currentItem.returns = currentItem.returns || []; + currentItem.returns.push({ + name: paramName === '' ? '' : paramName, + type: paramType, + description: paramDesc, + }); + } else if (precedingLines.includes('**Parameters**')) { + // Only add if it looks like a valid parameter (has a type or starts with underscore) + if (paramType || paramName.startsWith('_')) { + currentItem.params = currentItem.params || []; + currentItem.params.push({ + name: paramName, + type: paramType, + description: paramDesc, + }); + } + } + } + } + } + } + + // Save last item + if (currentItem) { + saveCurrentItem(data, currentItem, itemType); + } + + // Ensure overview is set + if (!data.overview) { + data.overview = data.description || `Documentation for ${data.title}.`; + } + + return data; +} + +/** + * Create a new item object based on section type + * @param {string} name - Item name + * @param {string} section - Section type + * @returns {object} New item object + */ +function createNewItem(name, section) { + const base = { + name, + description: '', + notice: '', + }; + + switch (section) { + case 'functions': + return { + ...base, + signature: '', + params: [], + returns: [], + mutability: 'nonpayable', + }; + case 'events': + return { + ...base, + signature: '', + params: [], + }; + case 'errors': + return { + ...base, + signature: '', + params: [], + }; + case 'structs': + return { + ...base, + definition: '', + fields: [], + }; + case 'stateVariables': + return { + ...base, + type: '', + value: '', + }; + default: + return base; + } +} + +/** + * Save current item to data object + * @param {object} data - Data object to save to + * @param {object} item - Item to save + * @param {string} type - Item type + */ +function saveCurrentItem(data, item, type) { + if (!type || !item) return; + + switch (type) { + case 'functions': + data.functions.push(item); + break; + case 'events': + data.events.push(item); + break; + case 'errors': + data.errors.push(item); + break; + case 'structs': + data.structs.push(item); + break; + case 'stateVariables': + data.stateVariables.push(item); + break; + } +} + +/** + * Sanitize markdown links that point to non-existent files + * Removes or converts broken links to plain text + * @param {string} text - Text that may contain markdown links + * @returns {string} Sanitized text + */ +function sanitizeBrokenLinks(text) { + if (!text) return text; + + // Remove markdown links that point to /src/ paths (forge doc links) + // Pattern: [text](/src/...) + return text.replace(/\[([^\]]+)\]\(\/src\/[^\)]+\)/g, '$1'); +} + +/** + * Clean description text by removing markdown artifacts + * Strips **Parameters**, **Returns**, **Note:** and other section markers + * that get incorrectly included in descriptions from forge doc output + * @param {string} text - Description text that may contain markdown artifacts + * @returns {string} Cleaned description text + */ +function cleanDescription(text) { + if (!text) return text; + + let cleaned = text; + + // Remove markdown section headers that shouldn't be in descriptions + // These patterns appear when forge doc parsing doesn't stop at section boundaries + const artifactPatterns = [ + /\s*\*\*Parameters\*\*\s*/g, + /\s*\*\*Returns\*\*\s*/g, + /\s*\*\*Note:\*\*\s*/g, + /\s*\*\*Events\*\*\s*/g, + /\s*\*\*Errors\*\*\s*/g, + /\s*\*\*See Also\*\*\s*/g, + /\s*\*\*Example\*\*\s*/g, + ]; + + for (const pattern of artifactPatterns) { + cleaned = cleaned.replace(pattern, ' '); + } + + // Remove @custom: tags that may leak through (e.g., "@custom:error AccessControlUnauthorizedAccount") + cleaned = cleaned.replace(/@custom:\w+\s+/g, ''); + + // Clean up "error: ErrorName" patterns that appear inline + // Keep the error name but format it better: "error: ErrorName If..." -> "Reverts with ErrorName if..." + cleaned = cleaned.replace(/\berror:\s+(\w+)\s+/gi, 'Reverts with $1 '); + + // Normalize whitespace: collapse multiple spaces, trim + cleaned = cleaned.replace(/\s+/g, ' ').trim(); + + return cleaned; +} + +/** + * Extract storage information from parsed data + * @param {object} data - Parsed documentation data + * @returns {string | null} Storage information or null + */ +function extractStorageInfo(data) { + // Look for STORAGE_POSITION in state variables + const storageVar = data.stateVariables.find(v => + v.name.includes('STORAGE') || v.name.includes('storage') + ); + + if (storageVar) { + return `Storage position: \`${storageVar.name}\` - ${storageVar.description || 'Used for diamond storage pattern.'}`; + } + + // Look for storage struct + const storageStruct = data.structs.find(s => + s.name.includes('Storage') + ); + + if (storageStruct) { + return `Uses the \`${storageStruct.name}\` struct following the ERC-8042 diamond storage pattern.`; + } + + return null; +} + +/** + * Detect item type from filename + * @param {string} filePath - Path to the markdown file + * @returns {string | null} Item type ('function', 'error', 'struct', 'event', 'enum', 'constants', or null) + */ +function detectItemTypeFromFilename(filePath) { + const basename = require('path').basename(filePath); + + if (basename.startsWith('function.')) return 'function'; + if (basename.startsWith('error.')) return 'error'; + if (basename.startsWith('struct.')) return 'struct'; + if (basename.startsWith('event.')) return 'event'; + if (basename.startsWith('enum.')) return 'enum'; + if (basename.startsWith('constants.')) return 'constants'; + + return null; +} + +/** + * Parse an individual item file (function, error, constant, etc.) + * @param {string} content - Markdown content from forge doc + * @param {string} filePath - Path to the markdown file + * @returns {object | null} Parsed item object or null if parsing fails + */ +function parseIndividualItemFile(content, filePath) { + const itemType = detectItemTypeFromFilename(filePath); + if (!itemType) { + return null; + } + + const lines = content.split('\n'); + let itemName = ''; + let gitSource = ''; + let description = ''; + let signature = ''; + let definition = ''; + let descriptionBuffer = []; + let inCodeBlock = false; + let codeBlockLines = []; + let params = []; + let returns = []; + let constants = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Parse title (# heading) + if (line.startsWith('# ') && !itemName) { + itemName = line.replace('# ', '').trim(); + continue; + } + + // Parse git source link + if (trimmedLine.startsWith('[Git Source]')) { + const match = trimmedLine.match(/\[Git Source\]\((.*?)\)/); + if (match) { + gitSource = config.normalizeGitSource(match[1]); + } + continue; + } + + // Parse code block + if (line.startsWith('```solidity')) { + inCodeBlock = true; + codeBlockLines = []; + i++; + while (i < lines.length && !lines[i].startsWith('```')) { + codeBlockLines.push(lines[i]); + i++; + } + const codeContent = codeBlockLines.join('\n').trim(); + + if (itemType === 'constants') { + // For constants, parse multiple constant definitions + // Format: "bytes32 constant NON_REENTRANT_SLOT = keccak256(...)" + // Handle both single and multiple constants in one code block + const constantMatches = codeContent.match(/(\w+(?:\s*\d+)?)\s+constant\s+(\w+)\s*=\s*(.+?)(?:\s*;)?/g); + if (constantMatches) { + for (const match of constantMatches) { + const parts = match.match(/(\w+(?:\s*\d+)?)\s+constant\s+(\w+)\s*=\s*(.+?)(?:\s*;)?$/); + if (parts) { + constants.push({ + name: parts[2], + type: parts[1], + value: parts[3].trim(), + description: descriptionBuffer.join(' ').trim(), + }); + } + } + } else { + // Single constant definition (more flexible regex) + const singleMatch = codeContent.match(/(\w+(?:\s*\d+)?)\s+constant\s+(\w+)\s*=\s*(.+?)(?:\s*;)?$/); + if (singleMatch) { + constants.push({ + name: singleMatch[2], + type: singleMatch[1], + value: singleMatch[3].trim(), + description: descriptionBuffer.join(' ').trim(), + }); + } + } + // Clear description buffer after processing constants + descriptionBuffer = []; + } else { + signature = codeContent; + } + inCodeBlock = false; + continue; + } + + // Parse constants with ### heading format + if (itemType === 'constants' && line.startsWith('### ')) { + const constantName = line.replace('### ', '').trim(); + // Clear description buffer for this constant (only text before this heading) + // Filter out code block delimiters and empty lines + const currentConstantDesc = descriptionBuffer + .filter(l => l && !l.trim().startsWith('```') && l.trim() !== '') + .join(' ') + .trim(); + descriptionBuffer = []; + + // Look ahead for code block (within next 15 lines) + let foundCodeBlock = false; + let codeBlockEndIndex = i; + for (let j = i + 1; j < lines.length && j < i + 15; j++) { + if (lines[j].startsWith('```solidity')) { + foundCodeBlock = true; + const constCodeLines = []; + j++; + while (j < lines.length && !lines[j].startsWith('```')) { + constCodeLines.push(lines[j]); + j++; + } + codeBlockEndIndex = j; // j now points to the line after closing ``` + const constCode = constCodeLines.join('\n').trim(); + // Match: type constant name = value + // Handle complex types like "bytes32", "uint256", etc. + const constMatch = constCode.match(/(\w+(?:\s*\d+)?)\s+constant\s+(\w+)\s*=\s*(.+?)(?:\s*;)?$/); + if (constMatch) { + constants.push({ + name: constantName, + type: constMatch[1], + value: constMatch[3].trim(), + description: currentConstantDesc, + }); + } else { + // Fallback: if no match, still add constant with name from heading + constants.push({ + name: constantName, + type: '', + value: constCode, + description: currentConstantDesc, + }); + } + break; + } + } + if (!foundCodeBlock) { + // No code block found, but we have a heading - might be a constant without definition + // This shouldn't happen in forge doc output, but handle it gracefully + constants.push({ + name: constantName, + type: '', + value: '', + description: currentConstantDesc, + }); + } else { + // Skip to the end of the code block (the loop will increment i, so we set it to one before) + i = codeBlockEndIndex - 1; + } + continue; + } + + // Collect description (text before code block or after title) + // Skip code block delimiters, empty lines, and markdown table separators + if (!inCodeBlock && trimmedLine && + !trimmedLine.startsWith('#') && + !trimmedLine.startsWith('[') && + !trimmedLine.startsWith('|') && + !trimmedLine.startsWith('```') && + trimmedLine !== '') { + if (itemType !== 'constants' || !line.startsWith('###')) { + descriptionBuffer.push(trimmedLine); + } + continue; + } + + // Parse table rows (Parameters or Returns) + if (trimmedLine.startsWith('|') && !trimmedLine.includes('----')) { + const cells = trimmedLine.split('|').map(c => c.trim()).filter(c => c); + + if (cells.length >= 3 && cells[0] !== 'Name' && cells[0] !== 'Parameter') { + const paramName = cells[0].replace(/`/g, '').trim(); + const paramType = cells[1].replace(/`/g, '').trim(); + const paramDesc = sanitizeBrokenLinks(cells[2] || ''); + + // Determine if Parameters or Returns based on preceding lines + const precedingLines = lines.slice(Math.max(0, i - 10), i).join('\n'); + + if (precedingLines.includes('**Returns**')) { + returns.push({ + name: paramName === '' ? '' : paramName, + type: paramType, + description: paramDesc, + }); + } else if (precedingLines.includes('**Parameters**')) { + if (paramType || paramName.startsWith('_')) { + params.push({ + name: paramName, + type: paramType, + description: paramDesc, + }); + } + } + } + } + } + + // Combine description buffer and clean it + if (descriptionBuffer.length > 0) { + description = cleanDescription(sanitizeBrokenLinks(descriptionBuffer.join(' ').trim())); + } + + // For constants, return array of constant objects + if (itemType === 'constants') { + return { + type: 'constants', + constants: constants.length > 0 ? constants : [{ + name: itemName || 'Constants', + type: '', + value: '', + description: description, + }], + gitSource: gitSource, + }; + } + + // For structs, use definition instead of signature + if (itemType === 'struct') { + definition = signature; + signature = ''; + } + + // Create item object based on type + const item = { + name: itemName, + description: description, + notice: description, + signature: signature, + definition: definition, + params: params, + returns: returns, + gitSource: gitSource, + }; + + // Add mutability for functions + if (itemType === 'function' && signature) { + if (signature.includes(' view ')) { + item.mutability = 'view'; + } else if (signature.includes(' pure ')) { + item.mutability = 'pure'; + } else if (signature.includes(' payable ')) { + item.mutability = 'payable'; + } else { + item.mutability = 'nonpayable'; + } + } + + return { + type: itemType, + item: item, + }; +} + +/** + * Aggregate multiple parsed items into a single data structure + * @param {Array} parsedItems - Array of parsed item objects from parseIndividualItemFile + * @param {string} sourceFilePath - Path to the source Solidity file + * @returns {object} Aggregated documentation data + */ +function aggregateParsedItems(parsedItems, sourceFilePath) { + const data = { + title: '', + description: '', + subtitle: '', + overview: '', + gitSource: '', + functions: [], + events: [], + errors: [], + structs: [], + stateVariables: [], + }; + + // Extract module name from source file path + const path = require('path'); + const basename = path.basename(sourceFilePath, '.sol'); + data.title = basename; + + // Extract git source from first item + for (const parsed of parsedItems) { + if (parsed && parsed.gitSource) { + data.gitSource = config.normalizeGitSource(parsed.gitSource); + break; + } + } + + // Group items by type + for (const parsed of parsedItems) { + if (!parsed) continue; + + if (parsed.type === 'function' && parsed.item) { + data.functions.push(parsed.item); + } else if (parsed.type === 'error' && parsed.item) { + data.errors.push(parsed.item); + } else if (parsed.type === 'event' && parsed.item) { + data.events.push(parsed.item); + } else if (parsed.type === 'struct' && parsed.item) { + data.structs.push(parsed.item); + } else if (parsed.type === 'enum' && parsed.item) { + // Enums can be treated as structs for display purposes + data.structs.push(parsed.item); + } else if (parsed.type === 'constants' && parsed.constants) { + // Add constants as state variables + for (const constant of parsed.constants) { + data.stateVariables.push({ + name: constant.name, + type: constant.type, + value: constant.value, + description: constant.description, + }); + } + } + } + + // Set default description if not provided + // Don't use item descriptions as module description - they'll be overridden by source file parsing + if (!data.description || + data.description.includes('Event emitted') || + data.description.includes('Thrown when') || + data.description.includes('function to') || + data.description.length < 20) { + data.description = `Documentation for ${data.title}`; + data.subtitle = data.description; + data.overview = data.description; + } + + return data; +} + +module.exports = { + parseForgeDocMarkdown, + extractStorageInfo, + parseIndividualItemFile, + aggregateParsedItems, + detectItemTypeFromFilename, +}; + + diff --git a/.github/scripts/generate-docs-utils/index-page-generator.js b/.github/scripts/generate-docs-utils/index-page-generator.js new file mode 100644 index 00000000..9b625aac --- /dev/null +++ b/.github/scripts/generate-docs-utils/index-page-generator.js @@ -0,0 +1,209 @@ +/** + * Index Page Generator + * + * Generates index.mdx files for category directories with custom DocCard components. + * This module provides utilities for creating styled category index pages. + */ + +const fs = require('fs'); +const path = require('path'); +const CONFIG = require('../config'); + +// ============================================================================ +// Category Items Discovery +// ============================================================================ + +/** + * Get all items (documents and subcategories) in a directory + * @param {string} outputDir - Directory to scan + * @param {string} relativePath - Relative path from library dir + * @param {Function} generateLabel - Function to generate labels from names + * @param {Function} generateDescription - Function to generate descriptions + * @returns {Array} Array of items with type, name, label, href, description + */ +function getCategoryItems(outputDir, relativePath, generateLabel, generateDescription) { + const items = []; + + if (!fs.existsSync(outputDir)) { + return items; + } + + const entries = fs.readdirSync(outputDir, { withFileTypes: true }); + + for (const entry of entries) { + // Skip hidden files, category files, and index files + if (entry.name.startsWith('.') || + entry.name === '_category_.json' || + entry.name === 'index.mdx') { + continue; + } + + if (entry.isFile() && entry.name.endsWith('.mdx')) { + // It's a document + const docName = entry.name.replace('.mdx', ''); + const docPath = path.join(outputDir, entry.name); + + // Try to read frontmatter for title and description + let title = generateLabel(docName); + let description = ''; + + try { + const content = fs.readFileSync(docPath, 'utf8'); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const titleMatch = frontmatter.match(/^title:\s*["']?(.*?)["']?$/m); + const descMatch = frontmatter.match(/^description:\s*["']?(.*?)["']?$/m); + if (titleMatch) title = titleMatch[1].trim(); + if (descMatch) description = descMatch[1].trim(); + } + } catch (error) { + // If reading fails, use defaults + } + + const docRelativePath = relativePath ? `${relativePath}/${docName}` : docName; + items.push({ + type: 'doc', + name: docName, + label: title, + description: description, + href: `/docs/library/${docRelativePath}`, + }); + } else if (entry.isDirectory()) { + // It's a subcategory + const subcategoryName = entry.name; + const subcategoryLabel = generateLabel(subcategoryName); + const subcategoryRelativePath = relativePath ? `${relativePath}/${subcategoryName}` : subcategoryName; + const subcategoryDescription = generateDescription(subcategoryName, relativePath.split('/')); + + items.push({ + type: 'category', + name: subcategoryName, + label: subcategoryLabel, + description: subcategoryDescription, + href: `/docs/library/${subcategoryRelativePath}`, + }); + } + } + + // Sort items: categories first, then docs, both alphabetically + items.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'category' ? -1 : 1; + } + return a.label.localeCompare(b.label); + }); + + return items; +} + +// ============================================================================ +// MDX Content Generation +// ============================================================================ + +/** + * Generate MDX content for a category index page + * @param {string} label - Category label + * @param {string} description - Category description + * @param {Array} items - Array of items to display + * @returns {string} Generated MDX content + */ +function generateIndexMdxContent(label, description, items) { + // Escape quotes in label and description for frontmatter + const escapedLabel = label.replace(/"/g, '\\"'); + const escapedDescription = description.replace(/"/g, '\\"'); + + let mdxContent = `--- +title: "${escapedLabel}" +description: "${escapedDescription}" +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ${escapedDescription} + + +`; + + if (items.length > 0) { + mdxContent += `\n`; + + for (const item of items) { + const iconName = item.type === 'category' ? 'package' : 'book'; + const itemDescription = item.description ? `"${item.description.replace(/"/g, '\\"')}"` : '""'; + + mdxContent += ` } + size="medium" + />\n`; + } + + mdxContent += `\n`; + } else { + mdxContent += `_No items in this category yet._\n`; + } + + return mdxContent; +} + +// ============================================================================ +// Index File Creation +// ============================================================================ + +/** + * Generate index.mdx file for a category + * @param {string} outputDir - Directory to create index file in + * @param {string} relativePath - Relative path from library dir + * @param {string} label - Category label + * @param {string} description - Category description + * @param {Function} generateLabel - Function to generate labels from names + * @param {Function} generateDescription - Function to generate descriptions + * @param {boolean} overwrite - Whether to overwrite existing files (default: false) + * @returns {boolean} True if file was created/updated, false if skipped + */ +function createCategoryIndexFile( + outputDir, + relativePath, + label, + description, + generateLabel, + generateDescription, + overwrite = false +) { + const indexFile = path.join(outputDir, 'index.mdx'); + + // Don't overwrite existing index files unless explicitly requested (allows manual customization) + if (!overwrite && fs.existsSync(indexFile)) { + return false; + } + + // Get items in this category + const items = getCategoryItems(outputDir, relativePath, generateLabel, generateDescription); + + // Generate MDX content + const mdxContent = generateIndexMdxContent(label, description, items); + + // Ensure directory exists + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(indexFile, mdxContent); + + return true; +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { + getCategoryItems, + generateIndexMdxContent, + createCategoryIndexFile, +}; + diff --git a/.github/scripts/generate-docs-utils/pr-body-generator.js b/.github/scripts/generate-docs-utils/pr-body-generator.js new file mode 100644 index 00000000..c37678cb --- /dev/null +++ b/.github/scripts/generate-docs-utils/pr-body-generator.js @@ -0,0 +1,104 @@ +/** + * PR Body Generator + * + * Generates a PR body from the docgen-summary.json file + * + * Usage: + * node pr-body-generator.js [summary-file-path] + * + * Outputs the PR body in GitHub Actions format to stdout + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * Generate PR body from summary data + * @param {Object} summary - Summary data from docgen-summary.json + * @returns {string} PR body markdown + */ +function generatePRBody(summary) { + const facets = summary.facets || []; + const modules = summary.modules || []; + const total = summary.totalGenerated || 0; + + let body = '## Auto-Generated Docs Pages\n\n'; + body += 'This PR contains auto-generated documentation from contract comments using `forge doc`. '; + body += 'The output is passed through AI to enhance the documentation content and add additional information.\n\n'; + body += '**Please ALWAYS review the generated content and ensure it is accurate and complete to Compose Standards.**\n'; + + + body += '### Summary\n'; + body += `- **Total generated:** ${total} files\n\n`; + + if (facets.length > 0) { + body += '### Facets\n'; + facets.forEach(facet => { + body += `- ${facet.title}\n`; + }); + body += '\n'; + } + + if (modules.length > 0) { + body += '### Modules\n'; + modules.forEach(module => { + body += `- ${module.title}\n`; + }); + body += '\n'; + } + + body += '### What was done\n'; + body += '1. Extracted NatSpec using `forge doc`\n'; + body += '2. Converted to Docusaurus MDX format\n'; + body += '3. Enhanced content with GitHub Copilot (optional)\n'; + body += '4. Verified documentation build\n\n'; + + body += '### Review Checklist\n'; + body += '- [ ] Review generated content for accuracy\n'; + body += '- [ ] Verify code examples are correct\n'; + body += '- [ ] Check for any missing documentation\n'; + body += '- [ ] Ensure consistency with existing docs\n\n'; + + body += '---\n'; + body += '🚨 **This PR was automatically generated. Please ALWAYS review before merging.**\n'; + body += `Generated on: ${new Date().toISOString()}\n`; + + return body; +} + +/** + * Main function + */ +function main() { + const summaryPath = process.argv[2] || 'docgen-summary.json'; + + if (!fs.existsSync(summaryPath)) { + console.error(`Error: Summary file not found: ${summaryPath}`); + process.exit(1); + } + + try { + const summaryContent = fs.readFileSync(summaryPath, 'utf8'); + const summary = JSON.parse(summaryContent); + + const prBody = generatePRBody(summary); + + // Output in GitHub Actions format + console.log('body</g, '>') + .replace(/\{/g, '{') + .replace(/\}/g, '}'); +} + +/** + * Convert object/array to a safe JavaScript expression for JSX attributes + * Returns the value wrapped in curly braces for direct use in JSX: {value} + * @param {*} obj - Value to convert + * @returns {string} JSX expression with curly braces: {JSON} + */ +function toJsxExpression(obj) { + if (obj == null) return '{null}'; + + try { + let jsonStr = JSON.stringify(obj); + // Ensure single line + jsonStr = jsonStr.replace(/[\n\r]/g, ' ').replace(/\s+/g, ' ').trim(); + // Verify it's valid JSON + JSON.parse(jsonStr); + // Return with JSX curly braces included + return `{${jsonStr}}`; + } catch (e) { + console.warn('Invalid JSON generated:', e.message); + return Array.isArray(obj) ? '{[]}' : '{{}}'; + } +} + +/** + * Escape special characters for JSX string attributes + * @param {string} str - String to escape + * @returns {string} Escaped string safe for JSX attributes + */ +function escapeJsx(str) { + if (!str) return ''; + + return sanitizeForMdx(str) + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/'/g, "\\'") + .replace(/\n/g, ' ') + .replace(/\{/g, '{') + .replace(/\}/g, '}') + // Don't escape backticks - they should be preserved for code formatting + .trim(); +} + +/** + * Escape markdown table special characters + * @param {string} str - String to escape + * @returns {string} Escaped string safe for markdown tables + */ +function escapeMarkdownTable(str) { + if (!str) return ''; + return str + .replace(/\|/g, '\\|') + .replace(/\n/g, ' ') + .replace(/\{/g, '{') + .replace(/\}/g, '}'); +} + +/** + * Escape HTML entities for safe display + * @param {string} str - String to escape + * @returns {string} HTML-escaped string + */ +function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Escape string for use in JavaScript/JSX object literal values + * Escapes quotes and backslashes for JavaScript strings (not HTML entities) + * Preserves backticks for code formatting + * @param {string} str - String to escape + * @returns {string} Escaped string safe for JavaScript string literals + */ +function escapeJsString(str) { + if (!str) return ''; + return String(str) + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/"/g, '\\"') // Escape double quotes + .replace(/'/g, "\\'") // Escape single quotes + .replace(/\n/g, '\\n') // Escape newlines + .replace(/\r/g, '\\r') // Escape carriage returns + .replace(/\t/g, '\\t'); // Escape tabs + // Note: Backticks are preserved for code formatting in descriptions +} + +/** + * Escape string for JSX string attributes, preserving backticks for code formatting + * This is specifically for descriptions that may contain code with backticks + * @param {string} str - String to escape + * @returns {string} Escaped string safe for JSX string attributes with preserved backticks + */ +function escapeJsxPreserveBackticks(str) { + if (!str) return ''; + + // Don't use sanitizeForMdx as it might HTML-escape things + // Just escape what's needed for JSX string attributes + return String(str) + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/"/g, '\\"') // Escape double quotes for JSX strings + .replace(/'/g, "\\'") // Escape single quotes + .replace(/\n/g, ' ') // Replace newlines with spaces + .replace(/\{/g, '{') // Escape curly braces for JSX + .replace(/\}/g, '}') // Escape curly braces for JSX + // Preserve backticks - don't escape them, they're needed for code formatting + .trim(); +} + +module.exports = { + escapeYaml, + escapeJsx, + escapeJsxPreserveBackticks, + sanitizeForMdx, + sanitizeMdx: sanitizeForMdx, // Alias for template usage + toJsxExpression, + escapeMarkdownTable, + escapeHtml, + escapeJsString, +}; + diff --git a/.github/scripts/generate-docs-utils/templates/package-lock.json b/.github/scripts/generate-docs-utils/templates/package-lock.json new file mode 100644 index 00000000..b1877ecc --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/package-lock.json @@ -0,0 +1,79 @@ +{ + "name": "compose-doc-templates", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "compose-doc-templates", + "version": "1.0.0", + "dependencies": { + "handlebars": "^4.7.8" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + } + } +} diff --git a/.github/scripts/generate-docs-utils/templates/package.json b/.github/scripts/generate-docs-utils/templates/package.json new file mode 100644 index 00000000..d5425ad4 --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/package.json @@ -0,0 +1,10 @@ +{ + "name": "compose-doc-templates", + "version": "1.0.0", + "private": true, + "description": "Template engine for generating MDX documentation", + "dependencies": { + "handlebars": "^4.7.8" + } +} + diff --git a/.github/scripts/generate-docs-utils/templates/pages/contract.mdx.template b/.github/scripts/generate-docs-utils/templates/pages/contract.mdx.template new file mode 100644 index 00000000..1723cadf --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/pages/contract.mdx.template @@ -0,0 +1,270 @@ +--- +sidebar_position: {{position}} +title: "{{escapeYaml title}}" +description: "{{escapeYaml description}}" +{{#if gitSource}} +gitSource: "{{gitSource}}" +{{/if}} +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + + +{{escapeYaml description}} + + +{{#if keyFeatures}} + +{{{sanitizeMdx keyFeatures}}} + +{{/if}} + +{{#if isModule}} + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + +{{/if}} + +## Overview + +{{{sanitizeMdx overview}}} + +--- + +## Storage + +{{#if hasStructs}} +{{#each structs}} +### {{name}} + +{{#if description}} +{{{sanitizeMdx description}}} +{{/if}} + +{{#if definition}} + +{{{codeContent definition}}} + +{{/if}} + +{{#unless @last}} +--- +{{/unless}} +{{/each}} +{{/if}} + +{{#if hasStorage}} + +{{#if hasStateVariables}} +### State Variables + + +{{/if}} +{{/if}} + +{{#if hasFunctions}} +## Functions + +{{#each functions}} +### {{name}} + +{{#if description}} +{{{sanitizeMdx description}}} +{{/if}} + +{{#if signature}} + +{{{codeContent signature}}} + +{{/if}} + +{{#if hasParams}} +**Parameters:** + + +{{/if}} + +{{#if hasReturns}} +**Returns:** + + +{{/if}} + +{{#unless @last}} +--- +{{/unless}} +{{/each}} +{{/if}} + +{{#if hasEvents}} +## Events + + +{{#each events}} + + {{#if description}} +
+ {{{sanitizeMdx description}}} +
+ {{/if}} + + {{#if signature}} +
+ Signature: + +{{{codeContent signature}}} + +
+ {{/if}} + + {{#if hasParams}} +
+ Parameters: + +
+ {{/if}} +
+{{/each}} +
+{{/if}} + +{{#if hasErrors}} +## Errors + + +{{#each errors}} + + {{#if description}} +
+ {{{sanitizeMdx description}}} +
+ {{/if}} + + {{#if signature}} +
+ Signature: + +{{signature}} + +
+ {{/if}} +
+{{/each}} +
+{{/if}} + +{{#if usageExample}} +## Usage Example + + +{{{codeContent usageExample}}} + +{{/if}} + +{{#if bestPractices}} +## Best Practices + + +{{{sanitizeMdx bestPractices}}} + +{{/if}} + +{{#if isFacet}} +{{#if securityConsiderations}} +## Security Considerations + + +{{{sanitizeMdx securityConsiderations}}} + +{{/if}} +{{/if}} + +{{#if isModule}} +{{#if integrationNotes}} +## Integration Notes + + +{{{sanitizeMdx integrationNotes}}} + +{{/if}} +{{/if}} + +
+ +
+ + + +{{#if relatedDocs}} + +{{/if}} diff --git a/.github/scripts/generate-docs-utils/templates/template-engine-handlebars.js b/.github/scripts/generate-docs-utils/templates/template-engine-handlebars.js new file mode 100644 index 00000000..18fb38d0 --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/template-engine-handlebars.js @@ -0,0 +1,262 @@ +/** + * Handlebars Template Engine for MDX Documentation Generation + * + * Replaces the custom template engine with Handlebars for better reliability + * and proper MDX formatting. + */ + +const Handlebars = require('handlebars'); +const fs = require('fs'); +const path = require('path'); +const helpers = require('./helpers'); + +// Track if helpers have been registered (only register once) +let helpersRegistered = false; + +/** + * Register custom helpers for Handlebars + * All helpers from helpers.js are registered for use in templates + */ +function registerHelpers() { + if (helpersRegistered) return; + + // Register escape helpers + Handlebars.registerHelper('escapeYaml', helpers.escapeYaml); + Handlebars.registerHelper('escapeJsx', helpers.escapeJsx); + // Helper to escape JSX strings while preserving backticks for code formatting + Handlebars.registerHelper('escapeJsxPreserveBackticks', function(value) { + if (!value) return ''; + const escaped = helpers.escapeJsxPreserveBackticks(value); + // Return as SafeString to prevent Handlebars from HTML-escaping backticks + return new Handlebars.SafeString(escaped); + }); + Handlebars.registerHelper('sanitizeMdx', helpers.sanitizeMdx); + Handlebars.registerHelper('escapeMarkdownTable', helpers.escapeMarkdownTable); + // Helper to escape value for JavaScript strings in JSX object literals + Handlebars.registerHelper('escapeJsString', function(value) { + if (!value) return ''; + const escaped = helpers.escapeJsString(value); + return new Handlebars.SafeString(escaped); + }); + + // Helper to emit a JSX style literal: returns a string like {{display: "flex", gap: "1rem"}} + Handlebars.registerHelper('styleLiteral', function(styles) { + if (!styles || typeof styles !== 'string') return '{{}}'; + + const styleObj = {}; + const pairs = styles.split(';').filter(pair => pair.trim()); + + pairs.forEach(pair => { + const [key, value] = pair.split(':').map(s => s.trim()); + if (key && value !== undefined) { + const camelKey = key.includes('-') + ? key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + : key; + const cleanValue = value.replace(/^["']|["']$/g, ''); + styleObj[camelKey] = cleanValue; + } + }); + + const entries = Object.entries(styleObj).map(([k, v]) => { + const isPureNumber = /^-?\d+\.?\d*$/.test(v.trim()); + // Quote everything except pure numbers + const valueLiteral = isPureNumber ? v : JSON.stringify(v); + return `${k}: ${valueLiteral}`; + }).join(', '); + + // Wrap with double braces so MDX sees style={{...}} + return `{{${entries}}}`; + }); + + // Helper to wrap code content in template literal for MDX + // This ensures MDX treats the content as a string, not JSX to parse + Handlebars.registerHelper('codeContent', function(content) { + if (!content) return '{``}'; + // Escape backticks in the content + const escaped = String(content).replace(/`/g, '\\`').replace(/\$/g, '\\$'); + return `{\`${escaped}\`}`; + }); + + // Helper to generate JSX style object syntax + // Accepts CSS string and converts to JSX object format + // Handles both kebab-case (margin-bottom) and camelCase (marginBottom) + Handlebars.registerHelper('jsxStyle', function(styles) { + if (!styles || typeof styles !== 'string') return '{}'; + + // Parse CSS string like "display: flex; margin-bottom: 1rem;" or "marginBottom: 1rem" + const styleObj = {}; + const pairs = styles.split(';').filter(pair => pair.trim()); + + pairs.forEach(pair => { + const [key, value] = pair.split(':').map(s => s.trim()); + if (key && value) { + // Convert kebab-case to camelCase if needed + const camelKey = key.includes('-') + ? key.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) + : key; + // Remove quotes from value if present + const cleanValue = value.replace(/^["']|["']$/g, ''); + styleObj[camelKey] = cleanValue; + } + }); + + // Convert to JSX object string with proper quoting + // All CSS values should be quoted as strings unless they're pure numbers + const entries = Object.entries(styleObj) + .map(([k, v]) => { + // Check if it's a pure number (integer or decimal without units) + if (/^-?\d+\.?\d*$/.test(v.trim())) { + return `${k}: ${v}`; + } + // Everything else (including CSS units like "0.75rem", "2rem", CSS vars, etc.) should be quoted + return `${k}: ${JSON.stringify(v)}`; + }) + .join(', '); + + // Return the object content wrapped in braces + // When used with {{{jsxStyle ...}}} in template, this becomes style={...} + // But we need style={{...}}, so we return with an extra opening brace + // The template uses {{{jsxStyle ...}}} which outputs raw, giving us style={{{...}}} + // To get style={{...}}, we need the helper to return {{...}} + // But with triple braces in template, we'd get style={{{{...}}}} which is wrong + // Solution: return just the object, template adds one brace manually + // Return the full JSX object expression with double braces + // Template will use raw block: {{{{jsxStyle ...}}}} + // This outputs: style={{{display: "flex", ...}}} + // But we need: style={{display: "flex", ...}} + // Actually, let's try: helper returns {{...}}, template uses {{jsxStyle}} (double) + // Handlebars will output the helper result + // But it will escape... unless we use raw block + // Simplest: return {{...}}, use {{{{jsxStyle}}}} raw block + return `{{${entries}}}`; + }); + + // Custom helper for better null/empty string handling + // Handlebars' default #if treats empty strings as falsy, but we want to be explicit + Handlebars.registerHelper('ifTruthy', function(value, options) { + if (value != null && + !(Array.isArray(value) && value.length === 0) && + !(typeof value === 'string' && value.trim().length === 0) && + !(typeof value === 'object' && Object.keys(value).length === 0)) { + return options.fn(this); + } + return options.inverse(this); + }); + + helpersRegistered = true; +} + +/** + * Normalize MDX formatting to ensure proper blank lines + * MDX requires blank lines between: + * - Import statements and JSX + * - JSX components and markdown + * - JSX components and other JSX + * + * @param {string} content - MDX content to normalize + * @returns {string} Properly formatted MDX + */ +function normalizeMdxFormatting(content) { + if (!content) return ''; + + let normalized = content; + + // 1. Ensure blank line after import statements (before JSX) + // Pattern: import ...;\n\n## + normalized = normalized.replace(/(\/>)\n(##)/g, '$1\n\n$2'); + + // 3. Ensure blank line after JSX closing tags (before other JSX) + // Pattern: \n)\n(<[A-Z])/g, '$1\n\n$2'); + + // 4. Ensure blank line after JSX closing tags (before markdown content) + // Pattern: \n## or \n[text] + normalized = normalized.replace(/(<\/[A-Z][a-zA-Z]+>)\n(##|[A-Z])/g, '$1\n\n$2'); + + // 5. Ensure blank line before JSX components (after markdown) + // Pattern: ]\n line.trimEnd()).join('\n'); + + // 8. Ensure file ends with single newline + normalized = normalized.trimEnd() + '\n'; + + return normalized; +} + +/** + * List available template files + * @returns {string[]} Array of template names (without extension) + */ +function listAvailableTemplates() { + const templatesDir = path.join(__dirname, 'pages'); + try { + return fs.readdirSync(templatesDir) + .filter(f => f.endsWith('.mdx.template')) + .map(f => f.replace('.mdx.template', '')); + } catch (e) { + return []; + } +} + +/** + * Load and render a template file with Handlebars + * @param {string} templateName - Name of template (without extension) + * @param {object} data - Data to render + * @returns {string} Rendered template with proper MDX formatting + * @throws {Error} If template cannot be loaded + */ +function loadAndRenderTemplate(templateName, data) { + const templatePath = path.join(__dirname, 'pages', `${templateName}.mdx.template`); + + if (!fs.existsSync(templatePath)) { + const available = listAvailableTemplates(); + throw new Error( + `Template '${templateName}' not found at: ${templatePath}\n` + + `Available templates: ${available.length > 0 ? available.join(', ') : 'none'}` + ); + } + + // Register helpers (only once, but safe to call multiple times) + registerHelpers(); + + try { + // Load template + const templateContent = fs.readFileSync(templatePath, 'utf8'); + + // Compile template with Handlebars + const template = Handlebars.compile(templateContent); + + // Render with data + let rendered = template(data); + + // Post-process: normalize MDX formatting + rendered = normalizeMdxFormatting(rendered); + + return rendered; + } catch (error) { + if (error.message.includes('Parse error')) { + throw new Error( + `Template parsing error in ${templateName}: ${error.message}\n` + + `Template path: ${templatePath}` + ); + } + throw error; + } +} + +module.exports = { + loadAndRenderTemplate, + registerHelpers, + listAvailableTemplates, +}; + diff --git a/.github/scripts/generate-docs-utils/templates/template-engine.js b/.github/scripts/generate-docs-utils/templates/template-engine.js new file mode 100644 index 00000000..b8e2ed22 --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/template-engine.js @@ -0,0 +1,366 @@ +/** + * Simple Template Engine + * + * A lightweight template engine with no external dependencies. + * + * Supports: + * - Variable substitution: {{variable}} (HTML escaped) + * - Unescaped output: {{{variable}}} (raw output) + * - Conditionals: {{#if variable}}...{{/if}} + * - Loops: {{#each array}}...{{/each}} + * - Helper functions: {{helperName variable}} or {{helperName(arg1, arg2)}} + * - Dot notation: {{object.property.nested}} + */ + +const fs = require('fs'); +const path = require('path'); + +// Import helpers from separate module +const helpers = require('./helpers'); +const { escapeHtml } = helpers; + +/** + * Get value from object using dot notation path + * @param {object} obj - Object to get value from + * @param {string} dotPath - Dot notation path (e.g., "user.name") + * @returns {*} Value at path or undefined + */ +function getValue(obj, dotPath) { + if (!dotPath || !obj) return undefined; + + const parts = dotPath.split('.'); + let value = obj; + + for (const part of parts) { + if (value == null) return undefined; + value = value[part]; + } + + return value; +} + +/** + * Check if a value is truthy for template conditionals + * - null/undefined → false + * - empty array → false + * - empty object → false + * - empty string → false + * - false → false + * - everything else → true + * + * @param {*} value - Value to check + * @returns {boolean} Whether value is truthy + */ +function isTruthy(value) { + if (value == null) return false; + if (Array.isArray(value)) return value.length > 0; + if (typeof value === 'object') return Object.keys(value).length > 0; + if (typeof value === 'string') return value.trim().length > 0; + return Boolean(value); +} + +/** + * Process a helper function call + * @param {string} helperName - Name of the helper + * @param {string[]} args - Argument strings (variable paths or literals) + * @param {object} context - Current template context + * @param {object} helperRegistry - Registry of helper functions + * @returns {string} Result of helper function + */ +function processHelper(helperName, args, context, helperRegistry) { + const helper = helperRegistry[helperName]; + if (!helper) { + console.warn(`Unknown template helper: ${helperName}`); + return ''; + } + + // Process arguments - can be variable paths or quoted literals + const processedArgs = args.map(arg => { + arg = arg.trim(); + // Check for quoted literal strings + if ((arg.startsWith('"') && arg.endsWith('"')) || + (arg.startsWith("'") && arg.endsWith("'"))) { + return arg.slice(1, -1); + } + // Otherwise treat as variable path + return getValue(context, arg); + }); + + return helper(...processedArgs); +} + +/** + * Process a variable expression (helper or simple variable) + * @param {string} expression - The expression inside {{ }} + * @param {object} context - Current template context + * @param {boolean} escapeOutput - Whether to HTML-escape the output + * @param {object} helperRegistry - Registry of helper functions + * @returns {string} Processed value + */ +function processExpression(expression, context, escapeOutput, helperRegistry) { + const expr = expression.trim(); + + // Check for helper with parentheses: helperName(arg1, arg2) + const parenMatch = expr.match(/^(\w+)\((.*)\)$/); + if (parenMatch) { + const [, helperName, argsStr] = parenMatch; + const args = argsStr ? argsStr.split(',').map(a => a.trim()) : []; + return processHelper(helperName, args, context, helperRegistry); + } + + // Check for helper with space: helperName variable + const spaceMatch = expr.match(/^(\w+)\s+(.+)$/); + if (spaceMatch && helperRegistry[spaceMatch[1]]) { + const [, helperName, arg] = spaceMatch; + return processHelper(helperName, [arg], context, helperRegistry); + } + + // Regular variable lookup + const value = getValue(context, expr); + if (value == null) return ''; + + const str = String(value); + return escapeOutput ? escapeHtml(str) : str; +} + +/** + * Find the matching closing tag for a block, handling nesting + * @param {string} content - Content to search + * @param {string} openTag - Opening tag pattern (e.g., '#if', '#each') + * @param {string} closeTag - Closing tag (e.g., '/if', '/each') + * @param {number} startPos - Position after the opening tag + * @returns {number} Position of the matching closing tag, or -1 if not found + */ +function findMatchingClose(content, openTag, closeTag, startPos) { + let depth = 1; + let pos = startPos; + + const openPattern = new RegExp(`\\{\\{${openTag}\\s+[^}]+\\}\\}`, 'g'); + const closePattern = new RegExp(`\\{\\{${closeTag}\\}\\}`, 'g'); + + while (depth > 0 && pos < content.length) { + // Find next open and close tags + openPattern.lastIndex = pos; + closePattern.lastIndex = pos; + + const openMatch = openPattern.exec(content); + const closeMatch = closePattern.exec(content); + + if (!closeMatch) { + return -1; // No matching close found + } + + // If open comes before close, increase depth + if (openMatch && openMatch.index < closeMatch.index) { + depth++; + pos = openMatch.index + openMatch[0].length; + } else { + depth--; + if (depth === 0) { + return closeMatch.index; + } + pos = closeMatch.index + closeMatch[0].length; + } + } + + return -1; +} + +/** + * Process nested conditionals: {{#if variable}}...{{/if}} + * @param {string} content - Template content + * @param {object} context - Data context + * @param {object} helperRegistry - Registry of helper functions + * @returns {string} Processed content + */ +function processConditionals(content, context, helperRegistry) { + let result = content; + const openPattern = /\{\{#if\s+([^}]+)\}\}/g; + + let match; + while ((match = openPattern.exec(result)) !== null) { + const condition = match[1].trim(); + const startPos = match.index; + const afterOpen = startPos + match[0].length; + + const closePos = findMatchingClose(result, '#if', '/if', afterOpen); + if (closePos === -1) { + console.warn(`Unmatched {{#if ${condition}}} at position ${startPos}`); + break; + } + + const ifContent = result.substring(afterOpen, closePos); + const closeEndPos = closePos + '{{/if}}'.length; + + // Evaluate condition and get replacement + const value = getValue(context, condition); + const replacement = isTruthy(value) + ? processContent(ifContent, context, helperRegistry) + : ''; + + // Replace in result + result = result.substring(0, startPos) + replacement + result.substring(closeEndPos); + + // Reset pattern to start from beginning since we modified the string + openPattern.lastIndex = 0; + } + + return result; +} + +/** + * Process nested loops: {{#each array}}...{{/each}} + * @param {string} content - Template content + * @param {object} context - Data context + * @param {object} helperRegistry - Registry of helper functions + * @returns {string} Processed content + */ +function processLoops(content, context, helperRegistry) { + let result = content; + const openPattern = /\{\{#each\s+([^}]+)\}\}/g; + + let match; + while ((match = openPattern.exec(result)) !== null) { + const arrayPath = match[1].trim(); + const startPos = match.index; + const afterOpen = startPos + match[0].length; + + const closePos = findMatchingClose(result, '#each', '/each', afterOpen); + if (closePos === -1) { + console.warn(`Unmatched {{#each ${arrayPath}}} at position ${startPos}`); + break; + } + + const loopContent = result.substring(afterOpen, closePos); + const closeEndPos = closePos + '{{/each}}'.length; + + // Get array and process each item + const array = getValue(context, arrayPath); + let replacement = ''; + + if (Array.isArray(array) && array.length > 0) { + replacement = array.map((item, index) => { + const itemContext = { ...context, ...item, index }; + return processContent(loopContent, itemContext, helperRegistry); + }).join(''); + } + + // Replace in result + result = result.substring(0, startPos) + replacement + result.substring(closeEndPos); + + // Reset pattern to start from beginning since we modified the string + openPattern.lastIndex = 0; + } + + return result; +} + +/** + * Process template content with the given context + * Handles all variable substitutions, helpers, conditionals, and loops + * + * IMPORTANT: Processing order matters! + * 1. Loops first - so nested conditionals are evaluated with correct item context + * 2. Conditionals second - after loops have expanded their content + * 3. Variables last - after all control structures are resolved + * + * @param {string} content - Template content to process + * @param {object} context - Data context + * @param {object} helperRegistry - Registry of helper functions + * @returns {string} Processed content + */ +function processContent(content, context, helperRegistry) { + let result = content; + + // 1. Process loops FIRST (handles nesting properly) + result = processLoops(result, context, helperRegistry); + + // 2. Process conditionals SECOND (handles nesting properly) + result = processConditionals(result, context, helperRegistry); + + // 3. Process triple braces for unescaped output: {{{variable}}} + const tripleBracePattern = /\{\{\{([^}]+)\}\}\}/g; + result = result.replace(tripleBracePattern, (match, expr) => { + return processExpression(expr, context, false, helperRegistry); + }); + + // 4. Process double braces for escaped output: {{variable}} + const doubleBracePattern = /\{\{([^}]+)\}\}/g; + result = result.replace(doubleBracePattern, (match, expr) => { + return processExpression(expr, context, true, helperRegistry); + }); + + return result; +} + +/** + * Render a template string with data + * @param {string} template - Template string + * @param {object} data - Data to render + * @returns {string} Rendered template + */ +function renderTemplate(template, data) { + if (!template) return ''; + if (!data) data = {}; + + return processContent(template, { ...data }, helpers); +} + +/** + * List available template files + * @returns {string[]} Array of template names (without extension) + */ +function listAvailableTemplates() { + const templatesDir = path.join(__dirname, 'pages'); + try { + return fs.readdirSync(templatesDir) + .filter(f => f.endsWith('.mdx.template')) + .map(f => f.replace('.mdx.template', '')); + } catch (e) { + return []; + } +} + +/** + * Load and render a template file + * @param {string} templateName - Name of template (without extension) + * @param {object} data - Data to render + * @returns {string} Rendered template + * @throws {Error} If template cannot be loaded + */ +function loadAndRenderTemplate(templateName, data) { + console.log('Loading template:', templateName); + console.log('Data:', data); + + const templatePath = path.join(__dirname, 'pages', `${templateName}.mdx.template`); + + try { + if (!fs.existsSync(templatePath)) { + const available = listAvailableTemplates(); + throw new Error( + `Template '${templateName}' not found at: ${templatePath}\n` + + `Available templates: ${available.length > 0 ? available.join(', ') : 'none'}` + ); + } + + const template = fs.readFileSync(templatePath, 'utf8'); + return renderTemplate(template, data); + } catch (error) { + if (error.code === 'ENOENT') { + const available = listAvailableTemplates(); + throw new Error( + `Template file not found: ${templatePath}\n` + + `Available templates: ${available.length > 0 ? available.join(', ') : 'none'}` + ); + } + throw error; + } +} + +module.exports = { + renderTemplate, + loadAndRenderTemplate, + getValue, + isTruthy, + listAvailableTemplates, +}; diff --git a/.github/scripts/generate-docs-utils/templates/templates.js b/.github/scripts/generate-docs-utils/templates/templates.js new file mode 100644 index 00000000..79c07b14 --- /dev/null +++ b/.github/scripts/generate-docs-utils/templates/templates.js @@ -0,0 +1,684 @@ +/** + * MDX Templates for Docusaurus documentation + * Uses Handlebars template engine for reliable MDX generation + */ + +const { loadAndRenderTemplate } = require('./template-engine-handlebars'); +const { sanitizeForMdx } = require('./helpers'); +const { readFileSafe } = require('../../workflow-utils'); + +/** + * Extract function parameters directly from Solidity source file + * @param {string} sourceFilePath - Path to the Solidity source file + * @param {string} functionName - Name of the function to extract parameters from + * @returns {Array} Array of parameter objects with name and type + */ +function extractParamsFromSource(sourceFilePath, functionName) { + if (!sourceFilePath || !functionName) return []; + + const sourceContent = readFileSafe(sourceFilePath); + if (!sourceContent) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Could not read source file: ${sourceFilePath}`); + } + return []; + } + + // Remove comments to avoid parsing issues + const withoutComments = sourceContent + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments + .replace(/\/\/.*$/gm, ''); // Remove line comments + + // Find function definition - match function name followed by opening parenthesis + // Handle both regular functions and free functions + const functionPattern = new RegExp( + `function\\s+${functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(([^)]*)\\)`, + 's' + ); + + const match = withoutComments.match(functionPattern); + if (!match || !match[1]) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Function ${functionName} not found in source file`); + } + return []; + } + + const paramsStr = match[1].trim(); + if (!paramsStr) { + return []; // Function has no parameters + } + + // Parse parameters - handle complex types like mappings, arrays, structs + const params = []; + let currentParam = ''; + let depth = 0; + let inString = false; + let stringChar = ''; + + for (let i = 0; i < paramsStr.length; i++) { + const char = paramsStr[i]; + + // Handle string literals + if ((char === '"' || char === "'") && (i === 0 || paramsStr[i - 1] !== '\\')) { + if (!inString) { + inString = true; + stringChar = char; + } else if (char === stringChar) { + inString = false; + } + currentParam += char; + continue; + } + + if (inString) { + currentParam += char; + continue; + } + + // Track nesting depth for generics, arrays, mappings + if (char === '<' || char === '[' || char === '(') { + depth++; + currentParam += char; + } else if (char === '>' || char === ']' || char === ')') { + depth--; + currentParam += char; + } else if (char === ',' && depth === 0) { + // Found a parameter boundary + const trimmed = currentParam.trim(); + if (trimmed) { + const parsed = parseParameter(trimmed); + if (parsed) { + params.push(parsed); + } + } + currentParam = ''; + } else { + currentParam += char; + } + } + + // Handle last parameter + const trimmed = currentParam.trim(); + if (trimmed) { + const parsed = parseParameter(trimmed); + if (parsed) { + params.push(parsed); + } + } + + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Extracted ${params.length} params from source for ${functionName}:`, JSON.stringify(params, null, 2)); + } + + return params; +} + +/** + * Parse a single parameter string into name and type + * @param {string} paramStr - Parameter string (e.g., "uint256 amount" or "address") + * @returns {object|null} Object with name and type, or null if invalid + */ +function parseParameter(paramStr) { + if (!paramStr || !paramStr.trim()) return null; + + // Remove storage location keywords + const cleaned = paramStr + .replace(/\b(memory|storage|calldata)\b/g, '') + .replace(/\s+/g, ' ') + .trim(); + + // Split by whitespace - last token is usually the name, rest is type + const parts = cleaned.split(/\s+/); + + if (parts.length === 0) return null; + + // If only one part, it's just a type (unnamed parameter) + if (parts.length === 1) { + return { name: '', type: parts[0], description: '' }; + } + + // Last part is the name, everything before is the type + const name = parts[parts.length - 1]; + const type = parts.slice(0, -1).join(' '); + + // Validate: name should be a valid identifier + if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) { + // If name doesn't look valid, treat the whole thing as a type + return { name: '', type: cleaned, description: '' }; + } + + return { name, type, description: '' }; +} + +/** + * Extract parameters from function signature string + * @param {string} signature - Function signature string + * @returns {Array} Array of parameter objects with name and type + */ +function extractParamsFromSignature(signature) { + if (!signature || typeof signature !== 'string') return []; + + // Match function parameters: function name(params) or just (params) + const paramMatch = signature.match(/\(([^)]*)\)/); + if (!paramMatch || !paramMatch[1]) return []; + + const paramsStr = paramMatch[1].trim(); + if (!paramsStr) return []; + + // Split by comma, but be careful with nested generics + const params = []; + let currentParam = ''; + let depth = 0; + + for (let i = 0; i < paramsStr.length; i++) { + const char = paramsStr[i]; + if (char === '<') depth++; + else if (char === '>') depth--; + else if (char === ',' && depth === 0) { + const trimmed = currentParam.trim(); + if (trimmed) { + // Parse "type name" or just "type" + const parts = trimmed.split(/\s+/); + if (parts.length >= 2) { + // Has both type and name + const type = parts.slice(0, -1).join(' '); + const name = parts[parts.length - 1]; + params.push({ name, type, description: '' }); + } else if (parts.length === 1) { + // Just type, no name + params.push({ name: '', type: parts[0], description: '' }); + } + } + currentParam = ''; + continue; + } + currentParam += char; + } + + // Handle last parameter + const trimmed = currentParam.trim(); + if (trimmed) { + const parts = trimmed.split(/\s+/); + if (parts.length >= 2) { + const type = parts.slice(0, -1).join(' '); + const name = parts[parts.length - 1]; + params.push({ name, type, description: '' }); + } else if (parts.length === 1) { + params.push({ name: '', type: parts[0], description: '' }); + } + } + + return params; +} + +/** + * Filter function parameters, removing invalid entries + * Invalid parameters include: empty names or names matching the function name (parsing error) + * @param {Array} params - Raw parameters array + * @param {string} functionName - Name of the function (to detect parsing errors) + * @returns {Array} Filtered and normalized parameters + */ +function filterAndNormalizeParams(params, functionName) { + return (params || []) + .filter(p => { + // Handle different possible data structures + const paramName = (p && (p.name || p.param || p.parameter || '')).trim(); + const paramType = (p && (p.type || p.paramType || '')).trim(); + + // Filter out parameters with empty or missing names + if (!paramName) return false; + // Filter out parameters where name matches function name (indicates parsing error) + if (paramName === functionName) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Filtered out invalid param: name="${paramName}" matches function name`); + } + return false; + } + // Filter out if type is empty AND name looks like it might be a function name (starts with lowercase, no underscore) + if (!paramType && /^[a-z]/.test(paramName) && !paramName.includes('_')) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Filtered out suspicious param: name="${paramName}" has no type`); + } + return false; + } + return true; + }) + .map(p => ({ + name: (p.name || p.param || p.parameter || '').trim(), + type: (p.type || p.paramType || '').trim(), + description: (p.description || p.desc || '').trim(), + })); +} + +/** + * Check if a function is internal by examining its signature + * @param {object} fn - Function data with signature property + * @returns {boolean} True if function is internal + */ +function isInternalFunction(fn) { + if (!fn || !fn.signature) return false; + + // Check if signature contains "internal" as a whole word + // Use word boundary regex to avoid matching "internalTransferFrom" etc. + const internalPattern = /\binternal\b/; + return internalPattern.test(fn.signature); +} + +/** + * Prepare function data for template rendering (shared between facet and module) + * @param {object} fn - Function data + * @param {string} sourceFilePath - Path to the Solidity source file + * @param {boolean} useSourceExtraction - Whether to try extracting params from source file (for modules) + * @returns {object} Prepared function data + */ +function prepareFunctionData(fn, sourceFilePath, useSourceExtraction = false) { + // Debug: log the raw function data + if (process.env.DEBUG_PARAMS) { + console.log(`\n[DEBUG] Function: ${fn.name}`); + console.log(`[DEBUG] Raw params:`, JSON.stringify(fn.params, null, 2)); + console.log(`[DEBUG] Signature:`, fn.signature); + } + + // Build parameters array, filtering out invalid parameters + let paramsArray = filterAndNormalizeParams(fn.params, fn.name); + + // If no valid parameters found, try extracting from source file (for modules) or signature + if (paramsArray.length === 0) { + // Try source file extraction for modules + if (useSourceExtraction && sourceFilePath) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] No valid params found, extracting from source file: ${sourceFilePath}`); + } + const extractedParams = extractParamsFromSource(sourceFilePath, fn.name); + if (extractedParams.length > 0) { + paramsArray = extractedParams; + } + } + + // Fallback to signature extraction if still no params + if (paramsArray.length === 0 && fn.signature) { + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] No valid params found, extracting from signature`); + } + const extractedParams = extractParamsFromSignature(fn.signature); + paramsArray = filterAndNormalizeParams(extractedParams, fn.name); + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Extracted params from signature:`, JSON.stringify(paramsArray, null, 2)); + } + } + } + + if (process.env.DEBUG_PARAMS) { + console.log(`[DEBUG] Final paramsArray:`, JSON.stringify(paramsArray, null, 2)); + } + + // Build returns array for table rendering + const returnsArray = (fn.returns || []).map(r => ({ + name: r.name || '-', + type: r.type, + description: r.description || '', + })); + + return { + name: fn.name, + signature: fn.signature, + description: fn.notice || fn.description || '', + params: paramsArray, + returns: returnsArray, + hasReturns: returnsArray.length > 0, + hasParams: paramsArray.length > 0, + }; +} + +/** + * Prepare event data for template rendering + * @param {object} event - Event data + * @returns {object} Prepared event data + */ +function prepareEventData(event) { + return { + name: event.name, + description: event.description || '', + signature: event.signature, + params: (event.params || []).map(p => ({ + name: p.name, + type: p.type, + description: p.description || '', + })), + hasParams: (event.params || []).length > 0, + }; +} + +/** + * Prepare error data for template rendering + * @param {object} error - Error data + * @returns {object} Prepared error data + */ +function prepareErrorData(error) { + return { + name: error.name, + description: error.description || '', + signature: error.signature, + }; +} + +/** + * Normalize struct definition indentation + * Ensures consistent 4-space indentation for struct body content + * @param {string} definition - Struct definition code + * @returns {string} Normalized struct definition with proper indentation + */ +function normalizeStructIndentation(definition) { + if (!definition) return definition; + + const lines = definition.split('\n'); + if (lines.length === 0) return definition; + + // Find the struct opening line (contains "struct" keyword) + let structStartIndex = -1; + let openingBraceOnSameLine = false; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes('struct')) { + structStartIndex = i; + openingBraceOnSameLine = lines[i].includes('{'); + break; + } + } + + if (structStartIndex === -1) return definition; + + // Get the indentation of the struct declaration line + const structLine = lines[structStartIndex]; + const structIndentMatch = structLine.match(/^(\s*)/); + const structIndent = structIndentMatch ? structIndentMatch[1] : ''; + + // Normalize all lines + const normalized = []; + let inStructBody = openingBraceOnSameLine; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + if (i === structStartIndex) { + // Keep struct declaration line as-is + normalized.push(line); + if (openingBraceOnSameLine) { + inStructBody = true; + } + continue; + } + + // Handle opening brace on separate line + if (!openingBraceOnSameLine && trimmed === '{') { + normalized.push(structIndent + '{'); + inStructBody = true; + continue; + } + + // Handle closing brace + if (trimmed === '}') { + normalized.push(structIndent + '}'); + inStructBody = false; + continue; + } + + // Skip empty lines + if (trimmed === '') { + normalized.push(''); + continue; + } + + // For struct body content, ensure 4-space indentation relative to struct declaration + if (inStructBody) { + // Remove any existing indentation and add proper indentation + const bodyIndent = structIndent + ' '; // 4 spaces + normalized.push(bodyIndent + trimmed); + } else { + // Keep lines outside struct body as-is + normalized.push(line); + } + } + + return normalized.join('\n'); +} + +/** + * Prepare struct data for template rendering + * @param {object} struct - Struct data + * @returns {object} Prepared struct data + */ +function prepareStructData(struct) { + return { + name: struct.name, + description: struct.description || '', + definition: normalizeStructIndentation(struct.definition), + }; +} + +/** + * Validate documentation data + * @param {object} data - Documentation data to validate + * @throws {Error} If data is invalid + */ +function validateData(data) { + if (!data || typeof data !== 'object') { + throw new Error('Invalid data: expected an object'); + } + if (!data.title || typeof data.title !== 'string') { + throw new Error('Invalid data: missing or invalid title'); + } +} + +/** + * Generate fallback description for state variables/constants based on naming patterns + * @param {string} name - Variable name (e.g., "STORAGE_POSITION", "DEFAULT_ADMIN_ROLE") + * @param {string} moduleName - Name of the module/contract for context + * @returns {string} Generated description or empty string + */ +function generateStateVariableDescription(name, moduleName) { + if (!name) return ''; + + const upperName = name.toUpperCase(); + + // Common patterns for diamond/ERC contracts + const patterns = { + // Storage position patterns + 'STORAGE_POSITION': 'Diamond storage slot position for this module', + 'STORAGE_SLOT': 'Diamond storage slot identifier', + '_STORAGE_POSITION': 'Diamond storage slot position', + '_STORAGE_SLOT': 'Diamond storage slot identifier', + + // Role patterns + 'DEFAULT_ADMIN_ROLE': 'Default administrative role identifier (bytes32(0))', + 'ADMIN_ROLE': 'Administrative role identifier', + 'MINTER_ROLE': 'Minter role identifier', + 'PAUSER_ROLE': 'Pauser role identifier', + 'BURNER_ROLE': 'Burner role identifier', + + // ERC patterns + 'INTERFACE_ID': 'ERC-165 interface identifier', + 'EIP712_DOMAIN': 'EIP-712 domain separator', + 'PERMIT_TYPEHASH': 'EIP-2612 permit type hash', + + // Reentrancy patterns + 'NON_REENTRANT_SLOT': 'Reentrancy guard storage slot', + '_NOT_ENTERED': 'Reentrancy status: not entered', + '_ENTERED': 'Reentrancy status: entered', + }; + + // Check exact matches first + if (patterns[upperName]) { + return patterns[upperName]; + } + + // Check partial matches + if (upperName.includes('STORAGE') && (upperName.includes('POSITION') || upperName.includes('SLOT'))) { + return 'Diamond storage slot position for this module'; + } + if (upperName.includes('_ROLE')) { + const roleName = name.replace(/_ROLE$/i, '').replace(/_/g, ' ').toLowerCase(); + return `${roleName.charAt(0).toUpperCase() + roleName.slice(1)} role identifier`; + } + if (upperName.includes('TYPEHASH')) { + return 'Type hash for EIP-712 structured data'; + } + if (upperName.includes('INTERFACE')) { + return 'ERC-165 interface identifier'; + } + + // Generic fallback + return ''; +} + +/** + * Prepare base data common to both facet and module templates + * @param {object} data - Documentation data + * @param {number} position - Sidebar position + * @returns {object} Base prepared data + */ +function prepareBaseData(data, position = 99) { + validateData(data); + + const description = data.description || `Contract documentation for ${data.title}`; + const subtitle = data.subtitle || data.description || `Contract documentation for ${data.title}`; + const overview = data.overview || data.description || `Documentation for ${data.title}.`; + + return { + position, + title: data.title, + description, + subtitle, + overview, + generatedDate: data.generatedDate || new Date().toISOString(), + gitSource: data.gitSource || '', + keyFeatures: data.keyFeatures || '', + usageExample: data.usageExample || '', + bestPractices: (data.bestPractices && data.bestPractices.trim()) ? data.bestPractices : null, + securityConsiderations: (data.securityConsiderations && data.securityConsiderations.trim()) ? data.securityConsiderations : null, + integrationNotes: (data.integrationNotes && data.integrationNotes.trim()) ? data.integrationNotes : null, + storageInfo: data.storageInfo || '', + + // Events + events: (data.events || []).map(prepareEventData), + hasEvents: (data.events || []).length > 0, + + // Errors + errors: (data.errors || []).map(prepareErrorData), + hasErrors: (data.errors || []).length > 0, + + // Structs + structs: (data.structs || []).map(prepareStructData), + hasStructs: (data.structs || []).length > 0, + + // State variables (for modules) - with fallback description generation + stateVariables: (data.stateVariables || []).map(v => { + const baseDescription = v.description || generateStateVariableDescription(v.name, data.title); + let description = baseDescription; + + // Append value to description if it exists and isn't already included + if (v.value && v.value.trim()) { + const valueStr = v.value.trim(); + // Check if value is already in description (case-insensitive) + // Escape special regex characters in valueStr + const escapedValue = valueStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Pattern matches "(Value: `...`)" or "(Value: ...)" format + const valuePattern = new RegExp('\\(Value:\\s*[`]?[^`)]*' + escapedValue + '[^`)]*[`]?\\)', 'i'); + if (!valuePattern.test(description)) { + // Format the value for display with backticks + // Use string concatenation to avoid template literal backtick issues + const valuePart = '(Value: `' + valueStr + '`)'; + description = baseDescription ? baseDescription + ' ' + valuePart : valuePart; + } + } + + return { + name: v.name, + type: v.type || '', + value: v.value || '', + description: description, + }; + }), + hasStateVariables: (data.stateVariables || []).length > 0, + hasStorage: Boolean(data.storageInfo || (data.stateVariables && data.stateVariables.length > 0)), + }; +} + +/** + * Prepare data for facet template rendering + * @param {object} data - Documentation data + * @param {number} position - Sidebar position + * @returns {object} Prepared data for facet template + */ +function prepareFacetData(data, position = 99) { + const baseData = prepareBaseData(data, position); + const sourceFilePath = data.sourceFilePath; + + // Filter out internal functions for facets (they act as pre-deploy logic blocks) + const publicFunctions = (data.functions || []).filter(fn => !isInternalFunction(fn)); + + return { + ...baseData, + // Contract type flags for unified template + isFacet: true, + isModule: false, + contractType: 'facet', + // Functions with APIReference-compatible format (no source extraction for facets) + // Only include non-internal functions since facets are pre-deploy logic blocks + functions: publicFunctions.map(fn => prepareFunctionData(fn, sourceFilePath, false)), + hasFunctions: publicFunctions.length > 0, + }; +} + +/** + * Prepare data for module template rendering + * @param {object} data - Documentation data + * @param {number} position - Sidebar position + * @returns {object} Prepared data for module template + */ +function prepareModuleData(data, position = 99) { + const baseData = prepareBaseData(data, position); + const sourceFilePath = data.sourceFilePath; + + return { + ...baseData, + // Contract type flags for unified template + isFacet: false, + isModule: true, + contractType: 'module', + // Functions with table-compatible format (with source extraction for modules) + functions: (data.functions || []).map(fn => prepareFunctionData(fn, sourceFilePath, true)), + hasFunctions: (data.functions || []).length > 0, + }; +} + +/** + * Generate complete facet documentation + * Uses the unified contract template with isFacet=true + * @param {object} data - Documentation data + * @param {number} position - Sidebar position + * @returns {string} Complete MDX document + */ +function generateFacetDoc(data, position = 99) { + const preparedData = prepareFacetData(data, position); + return loadAndRenderTemplate('contract', preparedData); +} + +/** + * Generate complete module documentation + * Uses the unified contract template with isModule=true + * @param {object} data - Documentation data + * @param {number} position - Sidebar position + * @returns {string} Complete MDX document + */ +function generateModuleDoc(data, position = 99) { + const preparedData = prepareModuleData(data, position); + return loadAndRenderTemplate('contract', preparedData); +} + +module.exports = { + generateFacetDoc, + generateModuleDoc, +}; diff --git a/.github/scripts/generate-docs.js b/.github/scripts/generate-docs.js new file mode 100644 index 00000000..b9970e3c --- /dev/null +++ b/.github/scripts/generate-docs.js @@ -0,0 +1,468 @@ +/** + * Docusaurus Documentation Generator + * + * Converts forge doc output to Docusaurus MDX format + * with optional AI enhancement. + * + * Features: + * - Mirrors src/ folder structure in documentation + * - Auto-generates category navigation files + * - AI-enhanced content generation + * + * Environment variables: + * GITHUB_TOKEN - GitHub token for AI API (optional) + * SKIP_ENHANCEMENT - Set to 'true' to skip AI enhancement + */ + +const fs = require('fs'); +const path = require('path'); +const { + getAllSolFiles, + findForgeDocFiles, + isInterface, + getContractType, + getOutputPath, + getSidebarPosition, + readChangedFilesFromFile, + extractModuleNameFromPath, + extractModuleDescriptionFromSource, + generateDescriptionFromName, +} = require('./generate-docs-utils/doc-generation-utils'); +const { readFileSafe, writeFileSafe } = require('./workflow-utils'); +const { + parseForgeDocMarkdown, + extractStorageInfo, + parseIndividualItemFile, + aggregateParsedItems, + detectItemTypeFromFilename, +} = require('./generate-docs-utils/forge-doc-parser'); +const { generateFacetDoc, generateModuleDoc } = require('./generate-docs-utils/templates/templates'); +const { enhanceWithAI, shouldSkipEnhancement, addFallbackContent } = require('./generate-docs-utils/ai-enhancement'); +const { syncDocsStructure, regenerateAllIndexFiles } = require('./generate-docs-utils/category-generator'); +const config = require('./generate-docs-utils/config'); + +// ============================================================================ +// Tracking +// ============================================================================ + +/** Track processed files for summary */ +const processedFiles = { + facets: [], + modules: [], + skipped: [], + errors: [], +}; + +// ============================================================================ +// Processing Functions +// ============================================================================ + +/** + * Process a single forge doc markdown file + * @param {string} forgeDocFile - Path to forge doc markdown file + * @param {string} solFilePath - Original .sol file path + * @returns {Promise} True if processed successfully + */ +async function processForgeDocFile(forgeDocFile, solFilePath) { + const content = readFileSafe(forgeDocFile); + if (!content) { + console.log(`Could not read: ${forgeDocFile}`); + processedFiles.errors.push({ file: forgeDocFile, error: 'Could not read file' }); + return false; + } + + // Parse the forge doc markdown + const data = parseForgeDocMarkdown(content, forgeDocFile); + + // Add source file path for parameter extraction + if (solFilePath) { + data.sourceFilePath = solFilePath; + } + + if (!data.title) { + console.log(`Could not parse title from: ${forgeDocFile}`); + processedFiles.skipped.push({ file: forgeDocFile, reason: 'No title found' }); + return false; + } + + // Skip interfaces + if (isInterface(data.title, content)) { + console.log(`Skipping interface: ${data.title}`); + processedFiles.skipped.push({ file: forgeDocFile, reason: 'Interface (filtered)' }); + return false; + } + + // Determine contract type + const contractType = getContractType(forgeDocFile, content); + console.log(`Type: ${contractType} - ${data.title}`); + + // Extract storage info for modules + if (contractType === 'module') { + data.storageInfo = extractStorageInfo(data); + } + + // Apply smart description fallback for facets with generic descriptions + if (contractType === 'facet') { + const looksLikeEnum = + data.description && + /\w+\s*=\s*\d+/.test(data.description) && + (data.description.match(/\w+\s*=\s*\d+/g) || []).length >= 2; + + const isGenericDescription = + !data.description || + data.description.startsWith('Contract documentation for') || + looksLikeEnum || + data.description.length < 20; + + if (isGenericDescription) { + const generatedDescription = generateDescriptionFromName(data.title); + if (generatedDescription) { + data.description = generatedDescription; + data.subtitle = generatedDescription; + data.overview = generatedDescription; + } + } + } + + // Get output path (mirrors src/ structure) + const pathInfo = getOutputPath(solFilePath, contractType); + + // Get smart sidebar position + data.position = getSidebarPosition(data.title); + + // Check if we should skip AI enhancement + const skipAIEnhancement = shouldSkipEnhancement(data) || process.env.SKIP_ENHANCEMENT === 'true'; + + // Enhance with AI if not skipped + let enhancedData = data; + if (!skipAIEnhancement) { + const token = process.env.GITHUB_TOKEN; + enhancedData = await enhanceWithAI(data, contractType, token); + } else { + console.log(`Skipping AI enhancement for ${data.title}`); + enhancedData = addFallbackContent(data, contractType); + } + + // Generate MDX content + const mdxContent = contractType === 'module' ? generateModuleDoc(enhancedData) : generateFacetDoc(enhancedData); + + // Ensure output directory exists + fs.mkdirSync(pathInfo.outputDir, { recursive: true }); + + // Write the file + if (writeFileSafe(pathInfo.outputFile, mdxContent)) { + console.log('✅ Generated:', pathInfo.outputFile); + + if (contractType === 'module') { + processedFiles.modules.push({ title: data.title, file: pathInfo.outputFile }); + } else { + processedFiles.facets.push({ title: data.title, file: pathInfo.outputFile }); + } + + return true; + } + + processedFiles.errors.push({ file: pathInfo.outputFile, error: 'Could not write file' }); + return false; +} + +/** + * Check if files need aggregation (individual item files vs contract-level files) + * @param {string[]} forgeDocFiles - Array of forge doc file paths + * @returns {boolean} True if files are individual items that need aggregation + */ +function needsAggregation(forgeDocFiles) { + for (const file of forgeDocFiles) { + const itemType = detectItemTypeFromFilename(file); + if (itemType) { + return true; + } + } + return false; +} + +/** + * Process aggregated files (for free function modules) + * @param {string[]} forgeDocFiles - Array of forge doc file paths + * @param {string} solFilePath - Original .sol file path + * @returns {Promise} True if processed successfully + */ +async function processAggregatedFiles(forgeDocFiles, solFilePath) { + console.log(`Aggregating ${forgeDocFiles.length} files for: ${solFilePath}`); + + const parsedItems = []; + let gitSource = ''; + + for (const forgeDocFile of forgeDocFiles) { + const content = readFileSafe(forgeDocFile); + if (!content) { + console.log(`Could not read: ${forgeDocFile}`); + continue; + } + + const parsed = parseIndividualItemFile(content, forgeDocFile); + if (parsed) { + parsedItems.push(parsed); + if (parsed.gitSource && !gitSource) { + gitSource = parsed.gitSource; + } + } + } + + if (parsedItems.length === 0) { + console.log(`No valid items found in files for: ${solFilePath}`); + processedFiles.errors.push({ file: solFilePath, error: 'No valid items parsed' }); + return false; + } + + const data = aggregateParsedItems(parsedItems, solFilePath); + + data.sourceFilePath = solFilePath; + + if (!data.title) { + data.title = extractModuleNameFromPath(solFilePath); + } + + // Try to get description from source file + const sourceDescription = extractModuleDescriptionFromSource(solFilePath); + if (sourceDescription) { + data.description = sourceDescription; + data.subtitle = sourceDescription; + data.overview = sourceDescription; + } else { + // Use smart description generator + const generatedDescription = generateDescriptionFromName(data.title); + if (generatedDescription) { + data.description = generatedDescription; + data.subtitle = generatedDescription; + data.overview = generatedDescription; + } else { + // Last resort fallback + const genericDescription = `Module providing internal functions for ${data.title}`; + if ( + !data.description || + data.description.includes('Event emitted') || + data.description.includes('Thrown when') + ) { + data.description = genericDescription; + data.subtitle = genericDescription; + data.overview = genericDescription; + } + } + } + + if (gitSource) { + data.gitSource = config.normalizeGitSource(gitSource); + } + + // Also normalize gitSource from aggregated data if present + if (data.gitSource) { + data.gitSource = config.normalizeGitSource(data.gitSource); + } + + const contractType = getContractType(solFilePath, ''); + console.log(`Type: ${contractType} - ${data.title}`); + + if (contractType === 'module') { + data.storageInfo = extractStorageInfo(data); + } + + // Get output path (mirrors src/ structure) + const pathInfo = getOutputPath(solFilePath, contractType); + + // Get smart sidebar position + data.position = getSidebarPosition(data.title); + + const skipAIEnhancement = shouldSkipEnhancement(data) || process.env.SKIP_ENHANCEMENT === 'true'; + + let enhancedData = data; + if (!skipAIEnhancement) { + const token = process.env.GITHUB_TOKEN; + enhancedData = await enhanceWithAI(data, contractType, token); + } else { + console.log(`Skipping AI enhancement for ${data.title}`); + enhancedData = addFallbackContent(data, contractType); + } + + // Generate MDX content + const mdxContent = contractType === 'module' ? generateModuleDoc(enhancedData) : generateFacetDoc(enhancedData); + + // Ensure output directory exists + fs.mkdirSync(pathInfo.outputDir, { recursive: true }); + + // Write the file + if (writeFileSafe(pathInfo.outputFile, mdxContent)) { + console.log('✅ Generated:', pathInfo.outputFile); + + if (contractType === 'module') { + processedFiles.modules.push({ title: data.title, file: pathInfo.outputFile }); + } else { + processedFiles.facets.push({ title: data.title, file: pathInfo.outputFile }); + } + + return true; + } + + processedFiles.errors.push({ file: pathInfo.outputFile, error: 'Could not write file' }); + return false; +} + +/** + * Process a Solidity source file + * @param {string} solFilePath - Path to .sol file + * @returns {Promise} + */ +async function processSolFile(solFilePath) { + console.log(`Processing: ${solFilePath}`); + + const forgeDocFiles = findForgeDocFiles(solFilePath); + + if (forgeDocFiles.length === 0) { + console.log(`No forge doc output found for: ${solFilePath}`); + processedFiles.skipped.push({ file: solFilePath, reason: 'No forge doc output' }); + return; + } + + if (needsAggregation(forgeDocFiles)) { + await processAggregatedFiles(forgeDocFiles, solFilePath); + } else { + for (const forgeDocFile of forgeDocFiles) { + console.log(`Reading: ${path.basename(forgeDocFile)}`); + await processForgeDocFile(forgeDocFile, solFilePath); + } + } +} + +// ============================================================================ +// Summary & Reporting +// ============================================================================ + +/** + * Print processing summary + */ +function printSummary() { + console.log('\n' + '='.repeat(50)); + console.log('Documentation Generation Summary'); + console.log('='.repeat(50)); + + console.log(`\nFacets generated: ${processedFiles.facets.length}`); + for (const f of processedFiles.facets) { + console.log(` - ${f.title}`); + } + + console.log(`\nModules generated: ${processedFiles.modules.length}`); + for (const m of processedFiles.modules) { + console.log(` - ${m.title}`); + } + + if (processedFiles.skipped.length > 0) { + console.log(`\nSkipped: ${processedFiles.skipped.length}`); + for (const s of processedFiles.skipped) { + console.log(` - ${path.basename(s.file)}: ${s.reason}`); + } + } + + if (processedFiles.errors.length > 0) { + console.log(`\nErrors: ${processedFiles.errors.length}`); + for (const e of processedFiles.errors) { + console.log(` - ${path.basename(e.file)}: ${e.error}`); + } + } + + const total = processedFiles.facets.length + processedFiles.modules.length; + console.log(`\nTotal generated: ${total} documentation files`); + console.log('='.repeat(50) + '\n'); +} + +/** + * Write summary to file for GitHub Action + */ +function writeSummaryFile() { + const summary = { + timestamp: new Date().toISOString(), + facets: processedFiles.facets, + modules: processedFiles.modules, + skipped: processedFiles.skipped, + errors: processedFiles.errors, + totalGenerated: processedFiles.facets.length + processedFiles.modules.length, + }; + + writeFileSafe('docgen-summary.json', JSON.stringify(summary, null, 2)); +} + +// ============================================================================ +// Main Entry Point +// ============================================================================ + +/** + * Main entry point + */ +async function main() { + console.log('Compose Documentation Generator\n'); + + // Step 1: Sync docs structure with src structure + console.log('📁 Syncing documentation structure with source...'); + const syncResult = syncDocsStructure(); + + if (syncResult.created.length > 0) { + console.log(` Created ${syncResult.created.length} new categories:`); + syncResult.created.forEach((c) => console.log(` ✅ ${c}`)); + } + console.log(` Total categories: ${syncResult.total}\n`); + + // Step 2: Determine which files to process + const args = process.argv.slice(2); + let solFiles = []; + + if (args.includes('--all')) { + console.log('Processing all Solidity files...'); + solFiles = getAllSolFiles(); + } else if (args.length > 0 && !args[0].startsWith('--')) { + const changedFilesPath = args[0]; + console.log(`Reading changed files from: ${changedFilesPath}`); + solFiles = readChangedFilesFromFile(changedFilesPath); + + if (solFiles.length === 0) { + console.log('No files in list, checking git diff...'); + const { getChangedSolFiles } = require('./generate-docs-utils/doc-generation-utils'); + solFiles = getChangedSolFiles(); + } + } else { + console.log('Getting changed Solidity files from git...'); + const { getChangedSolFiles } = require('./generate-docs-utils/doc-generation-utils'); + solFiles = getChangedSolFiles(); + } + + if (solFiles.length === 0) { + console.log('No Solidity files to process'); + return; + } + + console.log(`Found ${solFiles.length} Solidity file(s) to process\n`); + + // Step 3: Process each file + for (const solFile of solFiles) { + await processSolFile(solFile); + console.log(''); + } + + // Step 4: Regenerate all index pages now that docs are created + console.log('📄 Regenerating category index pages...'); + const indexResult = regenerateAllIndexFiles(true); + if (indexResult.regenerated.length > 0) { + console.log(` Regenerated ${indexResult.regenerated.length} index pages`); + if (indexResult.regenerated.length <= 10) { + indexResult.regenerated.forEach((c) => console.log(` ✅ ${c}`)); + } + } + console.log(''); + + // Step 5: Print summary + printSummary(); + writeSummaryFile(); +} + +main().catch((error) => { + console.error(`Fatal error: ${error}`); + process.exit(1); +}); diff --git a/.github/scripts/sync-docs-structure.js b/.github/scripts/sync-docs-structure.js new file mode 100644 index 00000000..4b4f833d --- /dev/null +++ b/.github/scripts/sync-docs-structure.js @@ -0,0 +1,210 @@ +#!/usr/bin/env node +/** + * Sync Documentation Structure + * + * Standalone script to mirror the src/ folder structure in website/docs/library/ + * Creates _category_.json files for Docusaurus navigation. + * + * Usage: + * node .github/scripts/sync-docs-structure.js [options] + * + * Options: + * --dry-run Show what would be created without making changes + * --verbose Show detailed output + * --help Show this help message + * + * Examples: + * node .github/scripts/sync-docs-structure.js + * node .github/scripts/sync-docs-structure.js --dry-run + */ + +const fs = require('fs'); +const path = require('path'); + +// Handle running from different directories +const scriptDir = __dirname; +process.chdir(path.join(scriptDir, '../..')); + +const { syncDocsStructure, scanSourceStructure } = require('./generate-docs-utils/category/category-generator'); + +// ============================================================================ +// CLI Parsing +// ============================================================================ + +const args = process.argv.slice(2); +const options = { + dryRun: args.includes('--dry-run'), + verbose: args.includes('--verbose'), + help: args.includes('--help') || args.includes('-h'), +}; + +// ============================================================================ +// Help +// ============================================================================ + +function showHelp() { + console.log(` +Sync Documentation Structure + +Mirrors the src/ folder structure in website/docs/library/ +Creates _category_.json files for Docusaurus navigation. + +Usage: + node .github/scripts/sync-docs-structure.js [options] + +Options: + --dry-run Show what would be created without making changes + --verbose Show detailed output + --help, -h Show this help message + +Examples: + node .github/scripts/sync-docs-structure.js + node .github/scripts/sync-docs-structure.js --dry-run +`); +} + +// ============================================================================ +// Tree Display +// ============================================================================ + +/** + * Display the source structure as a tree + * @param {Map} structure - Structure map from scanSourceStructure + */ +function displayTree(structure) { + console.log('\n📂 Source Structure (src/)\n'); + + // Sort by path for consistent display + const sorted = Array.from(structure.entries()).sort((a, b) => a[0].localeCompare(b[0])); + + // Build tree visualization + const tree = new Map(); + for (const [pathStr] of sorted) { + const parts = pathStr.split('/'); + let current = tree; + for (const part of parts) { + if (!current.has(part)) { + current.set(part, new Map()); + } + current = current.get(part); + } + } + + // Print tree + function printTree(node, prefix = '', isLast = true) { + const entries = Array.from(node.entries()); + entries.forEach(([name, children], index) => { + const isLastItem = index === entries.length - 1; + const connector = isLastItem ? '└── ' : '├── '; + const icon = children.size > 0 ? '📁' : '📄'; + console.log(`${prefix}${connector}${icon} ${name}`); + + if (children.size > 0) { + const newPrefix = prefix + (isLastItem ? ' ' : '│ '); + printTree(children, newPrefix, isLastItem); + } + }); + } + + printTree(tree); + console.log(''); +} + +// ============================================================================ +// Dry Run Mode +// ============================================================================ + +/** + * Simulate sync without making changes + * @param {Map} structure - Structure map + */ +function dryRun(structure) { + console.log('\n🔍 Dry Run Mode - No changes will be made\n'); + + const libraryDir = 'website/docs/library'; + let wouldCreate = 0; + let alreadyExists = 0; + + // Check base category + const baseCategoryFile = path.join(libraryDir, '_category_.json'); + if (fs.existsSync(baseCategoryFile)) { + console.log(` ✓ ${baseCategoryFile} (exists)`); + alreadyExists++; + } else { + console.log(` + ${baseCategoryFile} (would create)`); + wouldCreate++; + } + + // Check each category + for (const [relativePath] of structure) { + const categoryFile = path.join(libraryDir, relativePath, '_category_.json'); + if (fs.existsSync(categoryFile)) { + if (options.verbose) { + console.log(` ✓ ${categoryFile} (exists)`); + } + alreadyExists++; + } else { + console.log(` + ${categoryFile} (would create)`); + wouldCreate++; + } + } + + console.log(`\nSummary:`); + console.log(` Would create: ${wouldCreate} category files`); + console.log(` Already exist: ${alreadyExists} category files`); + console.log(`\nRun without --dry-run to apply changes.\n`); +} + +// ============================================================================ +// Main +// ============================================================================ + +function main() { + if (options.help) { + showHelp(); + return; + } + + console.log('📚 Sync Documentation Structure\n'); + console.log('Scanning src/ directory...'); + + const structure = scanSourceStructure(); + console.log(`Found ${structure.size} directories with Solidity files`); + + if (options.verbose || structure.size <= 20) { + displayTree(structure); + } + + if (options.dryRun) { + dryRun(structure); + return; + } + + console.log('Creating documentation structure...\n'); + const result = syncDocsStructure(); + + // Display results + console.log('='.repeat(50)); + console.log('Summary'); + console.log('='.repeat(50)); + console.log(`Created: ${result.created.length} categories`); + console.log(`Existing: ${result.existing.length} categories`); + console.log(`Total: ${result.total} categories`); + + if (result.created.length > 0) { + console.log('\nNewly created:'); + result.created.forEach((c) => console.log(` ✅ ${c}`)); + } + + console.log('\n✨ Done!\n'); + + // Show next steps + console.log('Next steps:'); + console.log(' 1. Run documentation generator to populate content:'); + console.log(' node .github/scripts/generate-docs.js --all\n'); + console.log(' 2. Or generate docs for specific files:'); + console.log(' node .github/scripts/generate-docs.js path/to/changed-files.txt\n'); +} + +main(); + diff --git a/.github/scripts/workflow-utils.js b/.github/scripts/workflow-utils.js index 0a254309..d95956d7 100644 --- a/.github/scripts/workflow-utils.js +++ b/.github/scripts/workflow-utils.js @@ -1,4 +1,5 @@ const fs = require('fs'); +const https = require('https'); const path = require('path'); const { execSync } = require('child_process'); @@ -63,18 +64,69 @@ function parsePRNumber(dataFileName) { } /** - * Read report file + * Read file content safely + * @param {string} filePath - Path to file (absolute or relative to workspace) + * @returns {string|null} File content or null if error + */ +function readFileSafe(filePath) { + try { + // If relative path, join with workspace if available + const fullPath = process.env.GITHUB_WORKSPACE && !path.isAbsolute(filePath) + ? path.join(process.env.GITHUB_WORKSPACE, filePath) + : filePath; + + if (!fs.existsSync(fullPath)) { + return null; + } + + return fs.readFileSync(fullPath, 'utf8'); + } catch (error) { + console.error(`Error reading file ${filePath}:`, error.message); + return null; + } +} + +/** + * Read report file (legacy - use readFileSafe for new code) * @param {string} reportFileName - Name of the report file * @returns {string|null} Report content or null if not found */ function readReport(reportFileName) { const reportPath = path.join(process.env.GITHUB_WORKSPACE, reportFileName); + return readFileSafe(reportPath); +} - if (!fs.existsSync(reportPath)) { - return null; +/** + * Ensure directory exists, create if not + * @param {string} dirPath - Directory path + */ +function ensureDir(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); } +} - return fs.readFileSync(reportPath, 'utf8'); +/** + * Write file safely + * @param {string} filePath - Path to file (absolute or relative to workspace) + * @param {string} content - Content to write + * @returns {boolean} True if successful + */ +function writeFileSafe(filePath, content) { + try { + // If relative path, join with workspace if available + const fullPath = process.env.GITHUB_WORKSPACE && !path.isAbsolute(filePath) + ? path.join(process.env.GITHUB_WORKSPACE, filePath) + : filePath; + + const dir = path.dirname(fullPath); + ensureDir(dir); + fs.writeFileSync(fullPath, content); + return true; + } catch (error) { + console.error(`Error writing file ${filePath}:`, error.message); + return false; + } } /** @@ -129,9 +181,61 @@ async function postOrUpdateComment(github, context, prNumber, body, commentMarke } } +/** + * Sleep for specified milliseconds + * @param {number} ms - Milliseconds to sleep + * @returns {Promise} + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Make HTTPS request (promisified) + * @param {object} options - Request options + * @param {string} body - Request body + * @returns {Promise} Response data + */ +function makeHttpsRequest(options, body) { + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + resolve(JSON.parse(data)); + } catch (e) { + resolve({ raw: data }); + } + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + + req.on('error', reject); + + if (body) { + req.write(body); + } + + req.end(); + }); +} + module.exports = { downloadArtifact, parsePRNumber, readReport, - postOrUpdateComment + readFileSafe, + writeFileSafe, + ensureDir, + postOrUpdateComment, + sleep, + makeHttpsRequest, }; \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs-build.yml similarity index 98% rename from .github/workflows/docs.yml rename to .github/workflows/docs-build.yml index 96ed2116..541d9366 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs-build.yml @@ -1,4 +1,4 @@ -name: Documentation +name: Build Docs on: pull_request: diff --git a/.github/workflows/docs-generate.yml b/.github/workflows/docs-generate.yml new file mode 100644 index 00000000..edf96990 --- /dev/null +++ b/.github/workflows/docs-generate.yml @@ -0,0 +1,187 @@ +name: Generate Docs + +on: + push: + branches: [main] + paths: + - 'src/**/*.sol' + workflow_dispatch: + inputs: + target_file: + description: 'Process ONLY the specified Solidity file(s) (relative path, e.g. src/contracts/MyFacet.sol or src/facets/A.sol,src/facets/B.sol)' + required: false + type: string + process_all: + description: 'Process ALL Solidity files' + required: false + default: false + type: boolean + skip_enhancement: + description: 'Skip AI Documentation Enhancement' + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + models: read # Required for GitHub Models API (AI enhancement) + +jobs: + generate-docs: + name: Generate Pages + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Get changed Solidity files + id: changed-files + run: | + # Prefer explicit target_file when provided via manual dispatch. + # You can pass a single file or a comma/space-separated list, e.g.: + # src/facets/A.sol,src/facets/B.sol + # src/facets/A.sol src/facets/B.sol + if [ -n "${{ github.event.inputs.target_file }}" ]; then + echo "Processing Solidity file(s) from input:" + echo "${{ github.event.inputs.target_file }}" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "process_all=false" >> $GITHUB_OUTPUT + # Normalize comma/space-separated list into one file path per line + echo "${{ github.event.inputs.target_file }}" \ + | tr ',' '\n' \ + | tr ' ' '\n' \ + | sed '/^$/d' \ + > /tmp/changed_sol_files.txt + elif [ "${{ github.event.inputs.process_all }}" == "true" ]; then + echo "Processing all Solidity files (manual trigger)" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "process_all=true" >> $GITHUB_OUTPUT + else + # Get list of changed .sol files compared to previous commit + CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- 'src/**/*.sol' 2>/dev/null || echo "") + + if [ -z "$CHANGED_FILES" ]; then + echo "No Solidity files changed" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "Changed files:" + echo "$CHANGED_FILES" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "process_all=false" >> $GITHUB_OUTPUT + + # Save to file for script + echo "$CHANGED_FILES" > /tmp/changed_sol_files.txt + fi + fi + + - name: Setup Node.js + if: steps.changed-files.outputs.has_changes == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Foundry + if: steps.changed-files.outputs.has_changes == 'true' + uses: foundry-rs/foundry-toolchain@v1 + + - name: Generate forge documentation + if: steps.changed-files.outputs.has_changes == 'true' + run: forge doc + + - name: Install template dependencies + if: steps.changed-files.outputs.has_changes == 'true' + working-directory: .github/scripts/generate-docs-utils/templates + run: npm install + + - name: Run documentation generator + if: steps.changed-files.outputs.has_changes == 'true' + env: + # AI Provider Configuration + GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SKIP_ENHANCEMENT: ${{ github.event.inputs.skip_enhancement || 'false' }} + run: | + if [ "${{ steps.changed-files.outputs.process_all }}" == "true" ]; then + node .github/scripts/generate-docs.js --all + else + node .github/scripts/generate-docs.js /tmp/changed_sol_files.txt + fi + + - name: Check for generated files + if: steps.changed-files.outputs.has_changes == 'true' + id: check-generated + run: | + # Check if any files were generated + if [ -f "docgen-summary.json" ]; then + TOTAL=$(cat docgen-summary.json | jq -r '.totalGenerated // 0' 2>/dev/null || echo "0") + if [ -n "$TOTAL" ] && [ "$TOTAL" -gt "0" ]; then + echo "has_generated=true" >> $GITHUB_OUTPUT + echo "Generated $TOTAL documentation files" + else + echo "has_generated=false" >> $GITHUB_OUTPUT + echo "No documentation files generated" + fi + else + echo "has_generated=false" >> $GITHUB_OUTPUT + fi + + - name: Verify documentation site build + if: steps.check-generated.outputs.has_generated == 'true' + working-directory: website + run: | + npm ci + npm run build + env: + ALGOLIA_APP_ID: 'dummy' + ALGOLIA_API_KEY: 'dummy' + ALGOLIA_INDEX_NAME: 'dummy' + POSTHOG_API_KEY: 'dummy' + continue-on-error: false + + - name: Generate PR body + if: steps.check-generated.outputs.has_generated == 'true' + id: pr-body + run: | + node .github/scripts/generate-docs-utils/pr-body-generator.js docgen-summary.json >> $GITHUB_OUTPUT + + - name: Clean up tmp files and stage website pages + if: steps.check-generated.outputs.has_generated == 'true' + run: | + # Remove forge docs folder (if it exists) + if [ -d "docs" ]; then + rm -rf docs + fi + + # Remove summary file (if it exists) + if [ -f "docgen-summary.json" ]; then + rm -f docgen-summary.json + fi + + # Reset any staged changes + git reset + + # Only stage website documentation files (force add in case they're ignored) + # Use library directory (the actual output directory) instead of contracts + if [ -d "website/docs/library" ]; then + git add -f website/docs/library/ + fi + + - name: Create Pull Request + if: steps.check-generated.outputs.has_generated == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: '[DOCS] Auto-generated Docs Pages' + commit-message: 'docs: auto-generate docs pages from NatSpec' + branch: docs/auto-generated-${{ github.run_number }} + body: ${{ steps.pr-body.outputs.body }} + labels: | + documentation + auto-generated + delete-branch: true + draft: true diff --git a/.gitignore b/.gitignore index 42f88272..521146a4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,24 +13,10 @@ out/ # Docusaurus # Dependencies -docs/node_modules - -# Production -docs/build - -# Generated files -docs/.docusaurus -docs/.cache-loader - -# Misc -docs/.DS_Store -docs/.env -docs/.env.local -docs/.env.development.local -docs/.env.test.local -docs/.env.production.local - -docs/npm-debug.log* -docs/yarn-debug.log* -docs/yarn-error.log* +website/node_modules +.github/scripts/generate-docs-utils/templates/node_modules +# Ignore forge docs output (root level only) +/docs/ +# Ignore Docs generation summary file +docgen-summary.json \ No newline at end of file diff --git a/website/README.md b/website/README.md index 23d9d30c..eff415d0 100644 --- a/website/README.md +++ b/website/README.md @@ -28,3 +28,9 @@ npm run build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Generate Facets & Modules Documentation + +```bash +npm run generate-docs +``` \ No newline at end of file diff --git a/website/docs/_category_.json b/website/docs/_category_.json index 8226f67a..e945adbe 100644 --- a/website/docs/_category_.json +++ b/website/docs/_category_.json @@ -3,8 +3,9 @@ "position": 1, "link": { "type": "generated-index", - "description": "Learn how to contribute to Compose" + "description": "Learn how to contribute to Compose", + "slug": "/docs" }, "collapsible": true, "collapsed": true -} \ No newline at end of file +} diff --git a/website/docs/contribution/_category_.json b/website/docs/contribution/_category_.json index 42c2e348..03a61040 100644 --- a/website/docs/contribution/_category_.json +++ b/website/docs/contribution/_category_.json @@ -3,8 +3,9 @@ "position": 5, "link": { "type": "generated-index", - "description": "Learn how to contribute to Compose" + "description": "Learn how to contribute to Compose", + "slug": "/docs/contribution" }, "collapsible": true, "collapsed": true -} \ No newline at end of file +} diff --git a/website/docs/getting-started/_category_.json b/website/docs/getting-started/_category_.json index 74b10c34..f17bd463 100644 --- a/website/docs/getting-started/_category_.json +++ b/website/docs/getting-started/_category_.json @@ -3,9 +3,9 @@ "position": 3, "link": { "type": "generated-index", - "description": "Learn how to install and configure Compose for your smart contract projects." + "description": "Learn how to install and configure Compose for your smart contract projects.", + "slug": "/docs/getting-started" }, "collapsible": true, "collapsed": true } - diff --git a/website/docs/library/_category_.json b/website/docs/library/_category_.json new file mode 100644 index 00000000..04125e1e --- /dev/null +++ b/website/docs/library/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Library", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/index" + } +} diff --git a/website/docs/library/access/AccessControl/AccessControlFacet.mdx b/website/docs/library/access/AccessControl/AccessControlFacet.mdx new file mode 100644 index 00000000..1cf4cc78 --- /dev/null +++ b/website/docs/library/access/AccessControl/AccessControlFacet.mdx @@ -0,0 +1,528 @@ +--- +sidebar_position: 99 +title: "AccessControlFacet" +description: "Manage roles and permissions within a Compose diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControl/AccessControlFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage roles and permissions within a Compose diamond. + + + +- Role-based access control (RBAC). +- Hierarchical role administration. +- Permission management for individual accounts. +- Batch operations for efficient role assignments and revocations. + + +## Overview + +The AccessControlFacet provides a robust role-based access control (RBAC) system for Compose diamonds. It allows for granular permission management, enabling fine-grained control over who can perform specific actions. This facet is crucial for securing administrative functions and ensuring proper governance. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +### State Variables + + + +## Functions + +### hasRole + +Returns if an account has a role. + + +{`function hasRole(bytes32 _role, address _account) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### requireRole + +Checks if an account has a required role. Reverts with AccessControlUnauthorizedAccount If the account does not have the role. + + +{`function requireRole(bytes32 _role, address _account) external view;`} + + +**Parameters:** + + + +--- +### getRoleAdmin + +Returns the admin role for a role. + + +{`function getRoleAdmin(bytes32 _role) external view returns (bytes32);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### setRoleAdmin + +Sets the admin role for a role. Emits a RoleAdminChanged event. Reverts with AccessControlUnauthorizedAccount If the caller is not the current admin of the role. + + +{`function setRoleAdmin(bytes32 _role, bytes32 _adminRole) external;`} + + +**Parameters:** + + + +--- +### grantRole + +Grants a role to an account. Emits a RoleGranted event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function grantRole(bytes32 _role, address _account) external;`} + + +**Parameters:** + + + +--- +### revokeRole + +Revokes a role from an account. Emits a RoleRevoked event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function revokeRole(bytes32 _role, address _account) external;`} + + +**Parameters:** + + + +--- +### grantRoleBatch + +Grants a role to multiple accounts in a single transaction. Emits a RoleGranted event for each newly granted account. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function grantRoleBatch(bytes32 _role, address[] calldata _accounts) external;`} + + +**Parameters:** + + + +--- +### revokeRoleBatch + +Revokes a role from multiple accounts in a single transaction. Emits a RoleRevoked event for each account the role is revoked from. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function revokeRoleBatch(bytes32 _role, address[] calldata _accounts) external;`} + + +**Parameters:** + + + +--- +### renounceRole + +Renounces a role from the caller. Emits a RoleRevoked event. Reverts with AccessControlUnauthorizedSender If the caller is not the account to renounce the role from. + + +{`function renounceRole(bytes32 _role, address _account) external;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when the admin role for a role is changed. +
+ +
+ Signature: + +{`event RoleAdminChanged(bytes32 indexed _role, bytes32 indexed _previousAdminRole, bytes32 indexed _newAdminRole);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a role is granted to an account. +
+ +
+ Signature: + +{`event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a role is revoked from an account. +
+ +
+ Signature: + +{`event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+ +
+ Thrown when the sender is not the account to renounce the role from. +
+ +
+ Signature: + +error AccessControlUnauthorizedSender(address _sender, address _account); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {DiamondCutFacet, DiamondLoupeFacet, FacetCutAction} from "@compose-protocol/diamond-contracts/contracts/facets/DiamondCutFacet.sol"; +import {AccessControlFacet} from "@compose-protocol/diamond-contracts/contracts/facets/AccessControlFacet.sol"; + +contract MyDiamond is DiamondCutFacet, DiamondLoupeFacet { + constructor(address _diamondAdmin) DiamondCutFacet(_diamondAdmin) {} + + // Function to add facets to the diamond (e.g., AccessControlFacet) + function upgrade() external { + // Assume AccessControlFacet is deployed at address + // and its ABI is known. + address accessControlFacetAddress = address(0x123...); + bytes calldata selector = abi.encodeWithSelector(AccessControlFacet.grantRole.selector); + + FacetCut[] memory cut = new FacetCut[](1); + cut[0] = FacetCut(accessControlFacetAddress, FacetCutAction.Add, new bytes4[](1)); + cut[0].functionSelectors[0] = selector; + + diamondCut(cut, address(0), ""); + } + + // Example of calling a function that requires a role + function grantAdminRoleTo(address _account) external { + // Assuming DEFAULT_ADMIN_ROLE is defined and accessible + AccessControlFacet(address(this)).grantRole(AccessControlFacet.DEFAULT_ADMIN_ROLE, _account); + } +} +`} + + +## Best Practices + + +- Integrate the AccessControlFacet early in the diamond deployment to establish administrative roles. +- Define custom roles specific to your diamond's logic and manage their administration carefully. +- Use batch functions (`grantRoleBatch`, `revokeRoleBatch`) for efficiency when managing multiple accounts or roles. + + +## Security Considerations + + +Ensure that the `DEFAULT_ADMIN_ROLE` is granted only to trusted addresses during deployment. Carefully manage role assignments to prevent unauthorized access to critical functions. The `renounceRole` function should be used with caution, as it permanently removes the caller's access to the role. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControl/AccessControlMod.mdx b/website/docs/library/access/AccessControl/AccessControlMod.mdx new file mode 100644 index 00000000..4db51ccc --- /dev/null +++ b/website/docs/library/access/AccessControl/AccessControlMod.mdx @@ -0,0 +1,446 @@ +--- +sidebar_position: 99 +title: "AccessControlMod" +description: "Manage roles and permissions within a diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControl/AccessControlMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage roles and permissions within a diamond. + + + +- Role-based access control for granular permission management. +- Functions for granting, revoking, and checking role assignments. +- Ability to set and manage administrative roles for other roles. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The AccessControl module provides a robust system for managing roles and permissions. It allows for granular control over which accounts can perform specific actions by assigning roles. This is crucial for building secure and upgradeable diamonds, ensuring that only authorized entities can modify critical state or execute sensitive functions. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +### State Variables + + + +## Functions + +### getStorage + +Returns the storage for the AccessControl. + + +{`function getStorage() pure returns (AccessControlStorage storage _s);`} + + +**Returns:** + + + +--- +### grantRole + +function to grant a role to an account. + + +{`function grantRole(bytes32 _role, address _account) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### hasRole + +function to check if an account has a role. + + +{`function hasRole(bytes32 _role, address _account) view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### requireRole + +function to check if an account has a required role. Reverts with AccessControlUnauthorizedAccount If the account does not have the role. + + +{`function requireRole(bytes32 _role, address _account) view;`} + + +**Parameters:** + + + +--- +### revokeRole + +function to revoke a role from an account. + + +{`function revokeRole(bytes32 _role, address _account) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### setRoleAdmin + +function to set the admin role for a role. + + +{`function setRoleAdmin(bytes32 _role, bytes32 _adminRole) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when the admin role for a role is changed. +
+ +
+ Signature: + +{`event RoleAdminChanged(bytes32 indexed _role, bytes32 indexed _previousAdminRole, bytes32 indexed _newAdminRole);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a role is granted to an account. +
+ +
+ Signature: + +{`event RoleGranted(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a role is revoked from an account. +
+ +
+ Signature: + +{`event RoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IAccessControl} from "@compose/contracts/modules/access-control/IAccessControl.sol"; + +contract MyFacet { + IAccessControl public immutable accessControl; + + constructor(address _diamondAddress) { + accessControl = IAccessControl(_diamondAddress); + } + + function grantAdminRole(address _account) external { + // Assuming DEFAULT_ADMIN_ROLE is defined elsewhere or is a constant + // For demonstration, let's assume it's a known role identifier + bytes32 constant DEFAULT_ADMIN_ROLE = keccak256("DEFAULT_ADMIN_ROLE"); + accessControl.grantRole(DEFAULT_ADMIN_ROLE, _account); + } + + function performAdminAction() external { + bytes32 constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + accessControl.requireRole(ADMIN_ROLE, msg.sender); + // ... perform admin action ... + } +}`} + + +## Best Practices + + +- Use `requireRole` to enforce access control checks at the beginning of external functions. +- Define roles using `keccak256("ROLE_NAME")` and manage them consistently. +- Store the `AccessControl` facet address in your diamond proxy for direct interaction. + + +## Integration Notes + + +The AccessControl module utilizes its own dedicated storage slot within the diamond's storage layout. Facets interact with this module through its `IAccessControl` interface. Changes made to role assignments via `grantRole` or `revokeRole` are immediately reflected and can be checked by any facet using `hasRole` or `requireRole`. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControl/_category_.json b/website/docs/library/access/AccessControl/_category_.json new file mode 100644 index 00000000..1504700a --- /dev/null +++ b/website/docs/library/access/AccessControl/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Access Control", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/AccessControl/index" + } +} diff --git a/website/docs/library/access/AccessControl/index.mdx b/website/docs/library/access/AccessControl/index.mdx new file mode 100644 index 00000000..5031c709 --- /dev/null +++ b/website/docs/library/access/AccessControl/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Access Control" +description: "Role-based access control (RBAC) pattern." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Role-based access control (RBAC) pattern. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/access/AccessControlPausable/AccessControlPausableFacet.mdx b/website/docs/library/access/AccessControlPausable/AccessControlPausableFacet.mdx new file mode 100644 index 00000000..18dc30da --- /dev/null +++ b/website/docs/library/access/AccessControlPausable/AccessControlPausableFacet.mdx @@ -0,0 +1,331 @@ +--- +sidebar_position: 99 +title: "AccessControlPausableFacet" +description: "Manage role-based access control and pause functionality." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControlPausable/AccessControlPausableFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage role-based access control and pause functionality. + + + +- Role-specific pausing and unpausing of functionality. +- Reverts with specific errors for unauthorized access and paused roles. +- Provides view functions to check role pause status. + + +## Overview + +This facet integrates role-based access control with pausing capabilities for specific roles. It allows administrators to temporarily disable roles, preventing any account from executing functions associated with a paused role. This provides granular control over operations during sensitive periods or maintenance. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +--- +### AccessControlPausableStorage + + +{`struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +}`} + + +### State Variables + + + +## Functions + +### isRolePaused + +Returns if a role is paused. + + +{`function isRolePaused(bytes32 _role) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### pauseRole + +Temporarily disables a role, preventing all accounts from using it. Only the admin of the role can pause it. Emits a RolePaused event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function pauseRole(bytes32 _role) external;`} + + +**Parameters:** + + + +--- +### unpauseRole + +Re-enables a role that was previously paused. Only the admin of the role can unpause it. Emits a RoleUnpaused event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function unpauseRole(bytes32 _role) external;`} + + +**Parameters:** + + + +--- +### requireRoleNotPaused + +Checks if an account has a role and if the role is not paused. - Reverts with AccessControlUnauthorizedAccount If the account does not have the role. - Reverts with AccessControlRolePaused If the role is paused. + + +{`function requireRoleNotPaused(bytes32 _role, address _account) external view;`} + + +**Parameters:** + + + +## Events + + + +
+ Event emitted when a role is paused. +
+ +
+ Signature: + +{`event RolePaused(bytes32 indexed _role, address indexed _account);`} + +
+ +
+ Parameters: + +
+
+ +
+ Event emitted when a role is unpaused. +
+ +
+ Signature: + +{`event RoleUnpaused(bytes32 indexed _role, address indexed _account);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+ +
+ Thrown when a role is paused and an operation requiring that role is attempted. +
+ +
+ Signature: + +error AccessControlRolePaused(bytes32 _role); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IAccessControlPausableFacet} from "@compose/contracts/facets/AccessControlPausable/IAccessControlPausableFacet.sol"; +import {IDiamondLoupe} from "@compose/contracts/facets/DiamondLoupe/IDiamondLoupe.sol"; + +contract AccessControlPausableConsumer { + address immutable DIAMOND_ADDRESS; + + constructor(address diamondAddress) { + DIAMOND_ADDRESS = diamondAddress; + } + + function pauseMyRole() external { + bytes4 selector = IAccessControlPausableFacet.pauseRole.selector; + (bool success, ) = DIAMOND_ADDRESS.call(abi.encodeWithSelector(selector, _MY_ROLE_ID)); + require(success, "Failed to pause role"); + } + + function isMyRolePaused() external view returns (bool) { + bytes4 selector = IAccessControlPausableFacet.isRolePaused.selector; + (bool success, bytes memory data) = DIAMOND_ADDRESS.call(abi.encodeWithSelector(selector, _MY_ROLE_ID)); + require(success, "Failed to check role paused status"); + return abi.decode(data, (bool)); + } + + // Assume _MY_ROLE_ID is defined elsewhere + uint256 private constant _MY_ROLE_ID = 1; +}`} + + +## Best Practices + + +- Ensure the caller has the appropriate administrative role before attempting to pause or unpause a role. +- Use `requireRoleNotPaused` within other facets or contract logic that relies on specific roles to enforce pause states. +- Store role IDs and administrative mappings securely within the diamond's storage. + + +## Security Considerations + + +Access to `pauseRole` and `unpauseRole` functions is restricted to the administrator of the respective role, preventing unauthorized pausing. The `requireRoleNotPaused` function ensures that operations tied to a role cannot be executed while that role is paused, mitigating risks during downtime or maintenance. Ensure role administration is managed securely to prevent accidental or malicious pausing. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControlPausable/AccessControlPausableMod.mdx b/website/docs/library/access/AccessControlPausable/AccessControlPausableMod.mdx new file mode 100644 index 00000000..7ae20ba3 --- /dev/null +++ b/website/docs/library/access/AccessControlPausable/AccessControlPausableMod.mdx @@ -0,0 +1,379 @@ +--- +sidebar_position: 99 +title: "AccessControlPausableMod" +description: "Manage role-based pausing for diamond functionality." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControlPausable/AccessControlPausableMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage role-based pausing for diamond functionality. + + + +- Role-specific pausing: Allows granular control over which operations are paused based on assigned roles. +- Integration with Access Control: Leverages existing role management for authorization checks. +- Reverts with specific errors: Differentiates between unauthorized access and paused roles for clearer debugging. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides role-based pausing capabilities, allowing specific roles to halt or resume certain operations within a diamond. It integrates with the AccessControl facet to enforce role checks before pausing or unpausing. This ensures that only authorized accounts can control the pause state of critical functions, enhancing operational safety and control. + +--- + +## Storage + +### AccessControlPausableStorage + + +{`struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +}`} + + +--- +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +### State Variables + + + +## Functions + +### getAccessControlStorage + +Returns the storage for AccessControl. + + +{`function getAccessControlStorage() pure returns (AccessControlStorage storage s);`} + + +**Returns:** + + + +--- +### getStorage + +Returns the storage for AccessControlPausable. + + +{`function getStorage() pure returns (AccessControlPausableStorage storage s);`} + + +**Returns:** + + + +--- +### isRolePaused + +function to check if a role is paused. + + +{`function isRolePaused(bytes32 _role) view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### pauseRole + +function to pause a role. + + +{`function pauseRole(bytes32 _role) ;`} + + +**Parameters:** + + + +--- +### requireRoleNotPaused + +function to check if an account has a role and if the role is not paused. **Notes:** - Reverts with AccessControlUnauthorizedAccount If the account does not have the role. - Reverts with AccessControlRolePaused If the role is paused. + + +{`function requireRoleNotPaused(bytes32 _role, address _account) view;`} + + +**Parameters:** + + + +--- +### unpauseRole + +function to unpause a role. + + +{`function unpauseRole(bytes32 _role) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Event emitted when a role is paused. +
+ +
+ Signature: + +{`event RolePaused(bytes32 indexed _role, address indexed _account);`} + +
+ +
+ Parameters: + +
+
+ +
+ Event emitted when a role is unpaused. +
+ +
+ Signature: + +{`event RoleUnpaused(bytes32 indexed _role, address indexed _account);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when a role is paused and an operation requiring that role is attempted. +
+ +
+ Signature: + +error AccessControlRolePaused(bytes32 _role); + +
+
+ +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IAccessControlPausableMod} from "../interfaces/IAccessControlPausableMod.sol"; + +contract MyFacet { + IAccessControlPausableMod public immutable accessControlPausableMod; + + constructor(address _accessControlPausableMod) { + accessControlPausableMod = IAccessControlPausableMod(_accessControlPausableMod); + } + + function _someRestrictedFunction() internal view { + // Ensure the caller has the 'OPERATOR' role and it's not paused + accessControlPausableMod.requireRoleNotPaused(msg.sender, keccak256("OPERATOR")); + + // ... function logic ... + } + + function _pauseOperatorRole() external { + // Only an admin can pause the OPERATOR role + // (Assuming an admin role is managed by AccessControl facet) + accessControlPausableMod.pauseRole(keccak256("OPERATOR")); + } + + function _unpauseOperatorRole() external { + accessControlPausableMod.unpauseRole(keccak256("OPERATOR")); + } +}`} + + +## Best Practices + + +- Use `requireRoleNotPaused` at the beginning of functions to enforce role presence and ensure the role is not paused before executing sensitive logic. +- Grant role pausing/unpausing permissions judiciously, typically to a highly privileged role like an admin. +- Implement custom errors or descriptive NatSpec for roles to improve clarity in access control checks. + + +## Integration Notes + + +The AccessControlPausableMod interacts with the diamond's storage to manage pause states for roles. Facets can call `requireRoleNotPaused` to enforce that a caller possesses a specific role and that the role is not currently paused. The module relies on the underlying AccessControl facet to manage role assignments. The state of paused roles is maintained within the module's storage, accessible via `getAccessControlStorage` and `getStorage`. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControlPausable/_category_.json b/website/docs/library/access/AccessControlPausable/_category_.json new file mode 100644 index 00000000..96418b00 --- /dev/null +++ b/website/docs/library/access/AccessControlPausable/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Pausable Access Control", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/AccessControlPausable/index" + } +} diff --git a/website/docs/library/access/AccessControlPausable/index.mdx b/website/docs/library/access/AccessControlPausable/index.mdx new file mode 100644 index 00000000..e5719126 --- /dev/null +++ b/website/docs/library/access/AccessControlPausable/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Pausable Access Control" +description: "RBAC with pause functionality." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + RBAC with pause functionality. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/access/AccessControlTemporal/AccessControlTemporalFacet.mdx b/website/docs/library/access/AccessControlTemporal/AccessControlTemporalFacet.mdx new file mode 100644 index 00000000..55738992 --- /dev/null +++ b/website/docs/library/access/AccessControlTemporal/AccessControlTemporalFacet.mdx @@ -0,0 +1,405 @@ +--- +sidebar_position: 99 +title: "AccessControlTemporalFacet" +description: "Manages time-bound role assignments within a diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControlTemporal/AccessControlTemporalFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages time-bound role assignments within a diamond. + + + +- Grants roles with specific expiry timestamps. +- Automatically enforces role expiry, revoking access upon expiration. +- Provides granular control over time-bound permissions. + + +## Overview + +The AccessControlTemporalFacet extends the diamond's access control capabilities by introducing time-bound role assignments. This facet allows administrators to grant roles that automatically expire, enhancing security and operational flexibility. It provides functions to manage these temporal roles and check their validity. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +--- +### AccessControlTemporalStorage + + +{`struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +}`} + + +### State Variables + + + +## Functions + +### getRoleExpiry + +Returns the expiry timestamp for a role assignment. + + +{`function getRoleExpiry(bytes32 _role, address _account) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### isRoleExpired + +Checks if a role assignment has expired. + + +{`function isRoleExpired(bytes32 _role, address _account) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### grantRoleWithExpiry + +Grants a role to an account with an expiry timestamp. Only the admin of the role can grant it with expiry. Emits a RoleGrantedWithExpiry event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) external;`} + + +**Parameters:** + + + +--- +### revokeTemporalRole + +Revokes a temporal role from an account. Only the admin of the role can revoke it. Emits a TemporalRoleRevoked event. Reverts with AccessControlUnauthorizedAccount If the caller is not the admin of the role. + + +{`function revokeTemporalRole(bytes32 _role, address _account) external;`} + + +**Parameters:** + + + +--- +### requireValidRole + +Checks if an account has a valid (non-expired) role. - Reverts with AccessControlUnauthorizedAccount If the account does not have the role. - Reverts with AccessControlRoleExpired If the role has expired. + + +{`function requireValidRole(bytes32 _role, address _account) external view;`} + + +**Parameters:** + + + +## Events + + + +
+ Event emitted when a role is granted with an expiry timestamp. +
+ +
+ Signature: + +{`event RoleGrantedWithExpiry( + bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Event emitted when a temporal role is revoked. +
+ +
+ Signature: + +{`event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+ +
+ Thrown when a role has expired. +
+ +
+ Signature: + +error AccessControlRoleExpired(bytes32 _role, address _account); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondLoupe} from "@compose/diamond-loupe/src/IDiamondLoupe.sol"; +import {AccessControlTemporalFacet as Facet} from "@compose/access-control-temporal/src/AccessControlTemporalFacet.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +contract MyDiamond is IDiamondLoupe, IERC165 { + // ... other facets + + function getAccessControlTemporalFacet() public view returns (Facet instance) { + bytes4 selector = Facet.grantRoleWithExpiry.selector; + address facetAddress = diamond.facetAddress(selector); + return Facet(facetAddress); + } + + function grantAdminRoleWithExpiry(address _account, bytes32 _role, uint64 _expiry) external { + Facet(diamond.facetAddress(Facet.grantRoleWithExpiry.selector)).grantRoleWithExpiry(_account, _role, _expiry); + } + + function isRoleCurrentlyValid(address _account, bytes32 _role) public view returns (bool) { + return !AccessControlRoleExpired.selector.is mencegahRoleExpired(_account, _role); + } + + // ... other diamond logic +}`} + + +## Best Practices + + +- Initialize temporal roles with appropriate expiry timestamps to prevent indefinite access. +- Regularly audit temporal role grants to ensure they align with current security policies. +- Utilize `revokeTemporalRole` for immediate revocation of expired or unnecessary temporal roles. + + +## Security Considerations + + +Ensure that the caller invoking `grantRoleWithExpiry` and `revokeTemporalRole` possesses the necessary administrative privileges for the target role to prevent unauthorized role management. Input validation for `_expiry` should be handled by the caller or a higher-level access control mechanism to prevent setting invalid or future-dated expiry times incorrectly. The facet relies on the underlying diamond's access control for initial authorization of administrative actions. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControlTemporal/AccessControlTemporalMod.mdx b/website/docs/library/access/AccessControlTemporal/AccessControlTemporalMod.mdx new file mode 100644 index 00000000..1b46e1d7 --- /dev/null +++ b/website/docs/library/access/AccessControlTemporal/AccessControlTemporalMod.mdx @@ -0,0 +1,474 @@ +--- +sidebar_position: 99 +title: "AccessControlTemporalMod" +description: "Manage time-bound role assignments for diamond access control." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/AccessControlTemporal/AccessControlTemporalMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage time-bound role assignments for diamond access control. + + + +- Grants roles with a specified expiry timestamp. +- Automatically checks for role expiry upon access validation. +- Provides functions to revoke temporal roles explicitly. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module extends standard access control by introducing time-bound role assignments. It allows for roles to be granted for a specific duration, automatically revoking them upon expiry. This enhances security and operational flexibility by enabling temporary permissions. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; + mapping(bytes32 role => bytes32 adminRole) adminRole; +}`} + + +--- +### AccessControlTemporalStorage + + +{`struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +}`} + + +### State Variables + + + +## Functions + +### getAccessControlStorage + +Returns the storage for AccessControl. + + +{`function getAccessControlStorage() pure returns (AccessControlStorage storage s);`} + + +**Returns:** + + + +--- +### getRoleExpiry + +function to get the expiry timestamp for a role assignment. + + +{`function getRoleExpiry(bytes32 _role, address _account) view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### getStorage + +Returns the storage for AccessControlTemporal. + + +{`function getStorage() pure returns (AccessControlTemporalStorage storage s);`} + + +**Returns:** + + + +--- +### grantRoleWithExpiry + +function to grant a role with an expiry timestamp. + + +{`function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### isRoleExpired + +function to check if a role assignment has expired. + + +{`function isRoleExpired(bytes32 _role, address _account) view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### requireValidRole + +function to check if an account has a valid (non-expired) role. **Notes:** - Reverts with AccessControlUnauthorizedAccount If the account does not have the role. - Reverts with AccessControlRoleExpired If the role has expired. + + +{`function requireValidRole(bytes32 _role, address _account) view;`} + + +**Parameters:** + + + +--- +### revokeTemporalRole + +function to revoke a temporal role. + + +{`function revokeTemporalRole(bytes32 _role, address _account) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +## Events + + + +
+ Event emitted when a role is granted with an expiry timestamp. +
+ +
+ Signature: + +{`event RoleGrantedWithExpiry( +bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Event emitted when a temporal role is revoked. +
+ +
+ Signature: + +{`event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when a role has expired. +
+ +
+ Signature: + +error AccessControlRoleExpired(bytes32 _role, address _account); + +
+
+ +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IAccessControlTemporalMod} from "@compose/contracts/modules/accessControl/IAccessControlTemporalMod.sol"; + +contract MyFacet { + IAccessControlTemporalMod internal accessControlTemporalMod; + + function initialize(address _accessControlTemporalModAddress) external { + accessControlTemporalMod = IAccessControlTemporalMod(_accessControlTemporalModAddress); + } + + function grantAdminRoleWithExpiry(address _account, uint64 _duration) external { + uint64 expiry = uint64(block.timestamp) + _duration; + accessControlTemporalMod.grantRoleWithExpiry(bytes32(0), _account, expiry); // Assuming role is 'DEFAULT_ADMIN_ROLE' + } + + function checkAdminRoleValidity(address _account) external view { + accessControlTemporalMod.requireValidRole(bytes32(0), _account); + } +}`} + + +## Best Practices + + +- Use `requireValidRole` before executing sensitive operations to ensure temporal roles are still active. +- Carefully manage role expiry durations to prevent accidental access loss or prolonged unauthorized access. +- Consider using custom errors for more granular revert reasons in your facet logic. + + +## Integration Notes + + +This module interacts with the diamond's storage to manage temporal role assignments. Facets calling `requireValidRole` will see the immediate effect of role grants or expirations. The module's storage should be initialized and accessible via the diamond proxy. Ensure the `AccessControlTemporalMod` facet is prioritized in the diamond's facet configuration if it relies on specific storage slots. + + +
+ +
+ + diff --git a/website/docs/library/access/AccessControlTemporal/_category_.json b/website/docs/library/access/AccessControlTemporal/_category_.json new file mode 100644 index 00000000..834b0b18 --- /dev/null +++ b/website/docs/library/access/AccessControlTemporal/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Temporal Access Control", + "position": 5, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/AccessControlTemporal/index" + } +} diff --git a/website/docs/library/access/AccessControlTemporal/index.mdx b/website/docs/library/access/AccessControlTemporal/index.mdx new file mode 100644 index 00000000..f8165020 --- /dev/null +++ b/website/docs/library/access/AccessControlTemporal/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Temporal Access Control" +description: "Time-limited role-based access control." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Time-limited role-based access control. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/access/Owner/OwnerFacet.mdx b/website/docs/library/access/Owner/OwnerFacet.mdx new file mode 100644 index 00000000..31695bf3 --- /dev/null +++ b/website/docs/library/access/Owner/OwnerFacet.mdx @@ -0,0 +1,197 @@ +--- +sidebar_position: 99 +title: "OwnerFacet" +description: "Manages contract ownership and transfer." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/Owner/OwnerFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages contract ownership and transfer. + + + +- Defines and manages the owner of the diamond. +- Supports transferring ownership to a new address. +- Allows for renouncing ownership, making the diamond effectively immutable from an owner perspective. + + +## Overview + +The OwnerFacet provides essential ownership management capabilities for a Compose diamond. It allows an owner to transfer ownership to a new address or renounce ownership entirely, ensuring control and security over the diamond's administrative functions. + +--- + +## Storage + +### OwnerStorage + + +{`struct OwnerStorage { + address owner; +}`} + + +### State Variables + + + +## Functions + +### owner + +Get the address of the owner + + +{`function owner() external view returns (address);`} + + +**Returns:** + + + +--- +### transferOwnership + +Set the address of the new owner of the contract Set _newOwner to address(0) to renounce any ownership. + + +{`function transferOwnership(address _newOwner) external;`} + + +**Parameters:** + + + +--- +### renounceOwnership + + +{`function renounceOwnership() external;`} + + +## Events + + + + +
+ Signature: + +{`event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error OwnerUnauthorizedAccount(); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IOwnerFacet} from "../facets/owner/IOwnerFacet.sol"; + +contract OwnerFacetDeployer { + // Assuming the diamond proxy address is known + address immutable DIAMOND_PROXY_ADDRESS; + + constructor(address _diamondProxyAddress) { + DIAMOND_PROXY_ADDRESS = _diamondProxyAddress; + } + + function getOwner() public view returns (address) { + bytes4 selector = IOwnerFacet.owner.selector; + (bool success, bytes memory data) = DIAMOND_PROXY_ADDRESS.call(abi.encodeWithSelector(selector)); + require(success, "Failed to get owner"); + return abi.decode(data, (address)); + } + + function transferOwnership(address _newOwner) public { + bytes4 selector = IOwnerFacet.transferOwnership.selector; + (bool success, ) = DIAMOND_PROXY_ADDRESS.call(abi.encodeWithSelector(selector, _newOwner)); + require(success, "Failed to transfer ownership"); + } + + function renounceOwnership() public { + bytes4 selector = IOwnerFacet.renounceOwnership.selector; + (bool success, ) = DIAMOND_PROXY_ADDRESS.call(abi.encodeWithSelector(selector)); + require(success, "Failed to renounce ownership"); + } +}`} + + +## Best Practices + + +- Initialize ownership during diamond deployment, typically to the deployer address. +- Use `transferOwnership` to delegate administrative control to another address. +- Consider using `renounceOwnership` only when administrative control is no longer required and the diamond should be immutable. + + +## Security Considerations + + +Access to `transferOwnership` and `renounceOwnership` is restricted to the current owner. Ensure the owner's private key is secured to prevent unauthorized changes. Setting the new owner to `address(0)` is the mechanism for renouncing ownership; this action is irreversible. The `owner` function is public and can be called by any address. + + +
+ +
+ + diff --git a/website/docs/library/access/Owner/OwnerMod.mdx b/website/docs/library/access/Owner/OwnerMod.mdx new file mode 100644 index 00000000..fcc3ab74 --- /dev/null +++ b/website/docs/library/access/Owner/OwnerMod.mdx @@ -0,0 +1,253 @@ +--- +sidebar_position: 99 +title: "OwnerMod" +description: "Manages ERC-173 contract ownership." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/Owner/OwnerMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages ERC-173 contract ownership. + + + +- Provides standard ERC-173 ownership tracking. +- Includes `owner()` and `requireOwner()` for access control. +- Supports safe ownership transfer and renouncement. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides essential ERC-173 contract ownership functionality, enabling secure management and transfer of ownership. It defines storage for the owner and provides functions to retrieve, check, set, and transfer ownership, crucial for access control within a diamond. + +--- + +## Storage + +### OwnerStorage + +storage-location: erc8042:compose.owner + + +{`struct OwnerStorage { + address owner; +}`} + + +### State Variables + + + +## Functions + +### getStorage + +Returns a pointer to the ERC-173 storage struct. Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + + +{`function getStorage() pure returns (OwnerStorage storage s);`} + + +**Returns:** + + + +--- +### owner + +Get the address of the owner + + +{`function owner() view returns (address);`} + + +**Returns:** + + + +--- +### requireOwner + +Reverts if the caller is not the owner. + + +{`function requireOwner() view;`} + + +--- +### setContractOwner + + +{`function setContractOwner(address _initialOwner) ;`} + + +**Parameters:** + + + +--- +### transferOwnership + +Set the address of the new owner of the contract Set _newOwner to address(0) to renounce any ownership. + + +{`function transferOwnership(address _newOwner) ;`} + + +**Parameters:** + + + +## Events + + + +
+ This emits when ownership of a contract changes. +
+ +
+ Signature: + +{`event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error OwnerAlreadyRenounced(); + +
+
+ + +
+ Signature: + +error OwnerUnauthorizedAccount(); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IOwnerMod} from "@compose/modules/owner/IOwnerMod.sol"; + +contract MyOwnerFacet { + address immutable DIAMOND_STORAGE_ADDRESS; + uint256 immutable OWNER_STORAGE_SLOT; + + constructor(address _diamondAddress, uint256 _ownerSlot) { + DIAMOND_STORAGE_ADDRESS = _diamondAddress; + OWNER_STORAGE_SLOT = _ownerSlot; + } + + function getOwner() external view returns (address) { + // Access OwnerMod storage directly using assembly + IOwnerMod.OwnerModStorage storage storagePtr = IOwnerMod.OwnerModStorage + (uint256(uint160(DIAMOND_STORAGE_ADDRESS)) + OWNER_STORAGE_SLOT); + return storagePtr.owner; + } + + function transferDiamondOwnership(address _newOwner) external { + // Call the internal OwnerMod function via the Diamond Proxy + IOwnerMod(DIAMOND_STORAGE_ADDRESS).transferOwnership(_newOwner); + } +}`} + + +## Best Practices + + +- Only the contract owner should call functions that modify ownership. +- Use `requireOwner()` to protect sensitive administrative functions. +- Be aware that renouncing ownership (setting owner to address(0)) is irreversible. + + +## Integration Notes + + +The OwnerMod utilizes a dedicated storage slot to store the `OwnerModStorage` struct, which contains the `owner` address. Facets interact with this storage either by directly accessing the slot via inline assembly (as demonstrated in `getStorage`) or by calling the module's functions through the diamond proxy. Changes to the owner are immediately reflected across all facets interacting with this module. + + +
+ +
+ + diff --git a/website/docs/library/access/Owner/_category_.json b/website/docs/library/access/Owner/_category_.json new file mode 100644 index 00000000..2ddf56c9 --- /dev/null +++ b/website/docs/library/access/Owner/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Owner", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/Owner/index" + } +} diff --git a/website/docs/library/access/Owner/index.mdx b/website/docs/library/access/Owner/index.mdx new file mode 100644 index 00000000..e619a378 --- /dev/null +++ b/website/docs/library/access/Owner/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Owner" +description: "Single-owner access control pattern." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Single-owner access control pattern. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsFacet.mdx b/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsFacet.mdx new file mode 100644 index 00000000..14a2dc43 --- /dev/null +++ b/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsFacet.mdx @@ -0,0 +1,201 @@ +--- +sidebar_position: 99 +title: "OwnerTwoStepsFacet" +description: "Owner Two Steps facet for Compose diamonds" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/OwnerTwoSteps/OwnerTwoStepsFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Owner Two Steps facet for Compose diamonds + + + +- Self-contained facet with no imports or inheritance +- Only `external` and `internal` function visibility +- Follows Compose readability-first conventions +- Ready for diamond integration + + +## Overview + +Owner Two Steps facet for Compose diamonds + +--- + +## Storage + +### OwnerStorage + + +{`struct OwnerStorage { + address owner; +}`} + + +--- +### PendingOwnerStorage + + +{`struct PendingOwnerStorage { + address pendingOwner; +}`} + + +### State Variables + + + +## Functions + +### owner + +Get the address of the owner + + +{`function owner() external view returns (address);`} + + +**Returns:** + + + +--- +### pendingOwner + +Get the address of the pending owner + + +{`function pendingOwner() external view returns (address);`} + + +**Returns:** + + + +--- +### transferOwnership + +Set the address of the new owner of the contract + + +{`function transferOwnership(address _newOwner) external;`} + + +**Parameters:** + + + +--- +### acceptOwnership + + +{`function acceptOwnership() external;`} + + +--- +### renounceOwnership + + +{`function renounceOwnership() external;`} + + +## Events + + + + +
+ Signature: + +{`event OwnershipTransferStarted(address indexed _previousOwner, address indexed _newOwner);`} + +
+ +
+ + +
+ Signature: + +{`event OwnershipTransferred(address indexed _previousOwner, address indexed _newOwner);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error OwnerUnauthorizedAccount(); + +
+
+
+ +
+ +
+ + diff --git a/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsMod.mdx b/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsMod.mdx new file mode 100644 index 00000000..45a3aa6c --- /dev/null +++ b/website/docs/library/access/OwnerTwoSteps/OwnerTwoStepsMod.mdx @@ -0,0 +1,302 @@ +--- +sidebar_position: 99 +title: "OwnerTwoStepsMod" +description: "Manages two-step contract ownership transfers securely." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/access/OwnerTwoSteps/OwnerTwoStepsMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages two-step contract ownership transfers securely. + + + +- Secure two-step ownership transfer to prevent accidental control loss. +- Explicit checks for owner and pending owner roles. +- Permissionless `renounceOwnership` option for relinquishing control. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module implements a secure two-step ownership transfer mechanism, preventing accidental loss of control. It ensures that ownership changes are explicitly confirmed by both the current and pending owners, enhancing the safety and auditability of diamond administration. + +--- + +## Storage + +### OwnerStorage + +storage-location: erc8042:compose.owner + + +{`struct OwnerStorage { + address owner; +}`} + + +--- +### PendingOwnerStorage + +storage-location: erc8042:compose.owner.pending + + +{`struct PendingOwnerStorage { + address pendingOwner; +}`} + + +### State Variables + + + +## Functions + +### acceptOwnership + +Finalizes ownership transfer; must be called by the pending owner. + + +{`function acceptOwnership() ;`} + + +--- +### getOwnerStorage + +Returns a pointer to the Owner storage struct. Uses inline assembly to access the storage slot defined by OWNER_STORAGE_POSITION. + + +{`function getOwnerStorage() pure returns (OwnerStorage storage s);`} + + +**Returns:** + + + +--- +### getPendingOwnerStorage + +Returns a pointer to the PendingOwner storage struct. Uses inline assembly to access the storage slot defined by PENDING_OWNER_STORAGE_POSITION. + + +{`function getPendingOwnerStorage() pure returns (PendingOwnerStorage storage s);`} + + +**Returns:** + + + +--- +### owner + +Returns the current owner. + + +{`function owner() view returns (address);`} + + +--- +### pendingOwner + +Returns the pending owner (if any). + + +{`function pendingOwner() view returns (address);`} + + +--- +### renounceOwnership + +Renounce ownership of the contract Sets the owner to address(0), disabling all functions restricted to the owner. + + +{`function renounceOwnership() ;`} + + +--- +### requireOwner + +Reverts if the caller is not the owner. + + +{`function requireOwner() view;`} + + +--- +### transferOwnership + +Initiates a two-step ownership transfer. + + +{`function transferOwnership(address _newOwner) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when ownership transfer is initiated (pending owner set). +
+ +
+ Signature: + +{`event OwnershipTransferStarted(address indexed _previousOwner, address indexed _newOwner);`} + +
+ +
+ +
+ Emitted when ownership transfer is finalized. +
+ +
+ Signature: + +{`event OwnershipTransferred(address indexed _previousOwner, address indexed _newOwner);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error OwnerAlreadyRenounced(); + +
+
+ + +
+ Signature: + +error OwnerUnauthorizedAccount(); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {OwnerTwoStepsMod} from "@compose-protocol/diamond-contracts/modules/ownership/OwnerTwoStepsMod.sol"; + +contract MyFacet is OwnerTwoStepsMod { + // Define storage slots for owner and pending owner + uint256 constant OWNER_STORAGE_POSITION = 1; + uint256 constant PENDING_OWNER_STORAGE_POSITION = 2; + + // Function to get the owner + function getOwner() public view returns (address) { + return owner(); + } + + // Function to initiate ownership transfer + function initiateTransfer(address _newOwner) external { + transferOwnership(_newOwner); + } + + // Function to accept ownership + function acceptOwnershipTransfer() external { + acceptOwnership(); + } + + // Function to renounce ownership + function renounceContractOwnership() external { + renounceOwnership(); + } +}`} + + +## Best Practices + + +- Always use `transferOwnership` to initiate a transfer, followed by `acceptOwnership` from the new owner. +- Implement `requireOwner` checks judiciously to protect critical administrative functions. +- Consider the implications of `renounceOwnership` as it renders all owner-restricted functions unusable. + + +## Integration Notes + + +This module utilizes specific storage slots for `Owner` and `PendingOwner`. Facets interacting with this module should be aware of these storage positions to correctly initialize and access ownership data. Ensure that the `OWNER_STORAGE_POSITION` and `PENDING_OWNER_STORAGE_POSITION` are defined and unique within your diamond's storage layout. The module relies on inline assembly to access these storage slots. + + +
+ +
+ + diff --git a/website/docs/library/access/OwnerTwoSteps/_category_.json b/website/docs/library/access/OwnerTwoSteps/_category_.json new file mode 100644 index 00000000..90b66a92 --- /dev/null +++ b/website/docs/library/access/OwnerTwoSteps/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Two-Step Owner", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/OwnerTwoSteps/index" + } +} diff --git a/website/docs/library/access/OwnerTwoSteps/index.mdx b/website/docs/library/access/OwnerTwoSteps/index.mdx new file mode 100644 index 00000000..bfade888 --- /dev/null +++ b/website/docs/library/access/OwnerTwoSteps/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Two-Step Owner" +description: "Two-step ownership transfer pattern." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Two-step ownership transfer pattern. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/access/_category_.json b/website/docs/library/access/_category_.json new file mode 100644 index 00000000..cbc9d5ba --- /dev/null +++ b/website/docs/library/access/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Access Control", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/access/index" + } +} diff --git a/website/docs/library/access/index.mdx b/website/docs/library/access/index.mdx new file mode 100644 index 00000000..edf619c1 --- /dev/null +++ b/website/docs/library/access/index.mdx @@ -0,0 +1,51 @@ +--- +title: "Access Control" +description: "Access control patterns for permission management in Compose diamonds." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Access control patterns for permission management in Compose diamonds. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/diamond/DiamondCutFacet.mdx b/website/docs/library/diamond/DiamondCutFacet.mdx new file mode 100644 index 00000000..87b8a1c9 --- /dev/null +++ b/website/docs/library/diamond/DiamondCutFacet.mdx @@ -0,0 +1,308 @@ +--- +sidebar_position: 99 +title: "DiamondCutFacet" +description: "Manage diamond facets and functions" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/DiamondCutFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage diamond facets and functions + + + +- Add, replace, and remove functions atomically using selectors. +- Supports executing an initialization function during a diamond cut. +- Manages function mappings and facet addresses within the diamond proxy. + + +## Overview + +The DiamondCutFacet enables programmatic management of a diamond's facets and functions. It allows adding, replacing, and removing functions, as well as executing an initialization function during a cut. This facet is crucial for upgrading and extending the diamond's functionality. + +--- + +## Storage + +### OwnerStorage + + +{`struct OwnerStorage { + address owner; +}`} + + +--- +### FacetAndPosition + + +{`struct FacetAndPosition { + address facet; + uint32 position; +}`} + + +--- +### DiamondStorage + + +{`struct DiamondStorage { + mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition; + /** + * Array of all function selectors that can be called in the diamond + */ + bytes4[] selectors; +}`} + + +--- +### FacetCut + + +{`struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; +}`} + + +### State Variables + + + +## Functions + +### diamondCut + +Add/replace/remove any number of functions and optionally execute a function with delegatecall + + +{`function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external;`} + + +**Parameters:** + + + +## Events + + + + +
+ Signature: + +{`event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error OwnerUnauthorizedAccount(); + +
+
+ + +
+ Signature: + +error NoSelectorsProvidedForFacet(address _facet); + +
+
+ + +
+ Signature: + +error NoBytecodeAtAddress(address _contractAddress, string _message); + +
+
+ + +
+ Signature: + +error RemoveFacetAddressMustBeZeroAddress(address _facet); + +
+
+ + +
+ Signature: + +error IncorrectFacetCutAction(uint8 _action); + +
+
+ + +
+ Signature: + +error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceImmutableFunction(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceFunctionWithTheSameFunctionFromTheSameFacet(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceFunctionThatDoesNotExists(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotRemoveFunctionThatDoesNotExist(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotRemoveImmutableFunction(bytes4 _selector); + +
+
+ + +
+ Signature: + +error InitializationFunctionReverted(address _initializationContractAddress, bytes _calldata); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondCut} from "@compose-protocol/diamond/contracts/facets/IDiamondCut.sol"; + +contract DiamondManager { + address immutable DIAMOND_ADDRESS; + + constructor(address diamondAddress) { + DIAMOND_ADDRESS = diamondAddress; + } + + function upgradeDiamond() external { + bytes32[] memory selectors = new bytes32[](1); + selectors[0] = IDiamondCut.addFunctions.selector; + + address[] memory facetAddresses = new address[](1); + facetAddresses[0] = address(this); // Assuming this contract deploys the new facet + + IDiamondCut(DIAMOND_ADDRESS).diamondCut(facetCuts, address(0), ""); // Placeholder for actual cuts + } + + // ... other deployment and upgrade logic ... +}`} + + +## Best Practices + + +- Use `diamondCut` with extreme care, as it modifies the diamond's core functionality. +- Ensure new facets are properly initialized if required, and the initialization function is correctly specified. +- Store the `DIAMOND_ADDRESS` in your deployment scripts or contracts to interact with the `DiamondCutFacet`. + + +## Security Considerations + + +Access to `diamondCut` should be restricted to authorized addresses (e.g., the diamond's owner) to prevent unauthorized modifications. Incorrectly specified selectors or facet addresses can lead to broken functionality or loss of access. Initialization functions must be carefully audited to prevent reentrancy or state corruption. + + +
+ +
+ + diff --git a/website/docs/library/diamond/DiamondCutMod.mdx b/website/docs/library/diamond/DiamondCutMod.mdx new file mode 100644 index 00000000..3a19fa17 --- /dev/null +++ b/website/docs/library/diamond/DiamondCutMod.mdx @@ -0,0 +1,393 @@ +--- +sidebar_position: 99 +title: "DiamondCutMod" +description: "Manage diamond facets and function pointers." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/DiamondCutMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage diamond facets and function pointers. + + + +- Supports adding, replacing, and removing functions from a diamond. +- Allows execution of an initialization function during a diamond cut. +- Provides granular control over facet management through selectors. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The DiamondCut module provides essential functions for managing the facets of a Compose diamond. It allows for the addition, replacement, and removal of functions, enabling dynamic upgrades and modifications to the diamond's capabilities. This module is crucial for maintaining and evolving the diamond's logic in a composable and upgrade-safe manner. + +--- + +## Storage + +### FacetCutAction + +Add=0, Replace=1, Remove=2 + +--- +### DiamondStorage + +storage-location: erc8042:compose.diamond + + +{`struct DiamondStorage { + mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition; + /** + * Array of all function selectors that can be called in the diamond + */ + bytes4[] selectors; +}`} + + +--- +### FacetAndPosition + + +{`struct FacetAndPosition { + address facet; + uint32 position; +}`} + + +--- +### FacetCut + + +{`struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; +}`} + + +### State Variables + + + +## Functions + +### addFunctions + + +{`function addFunctions(address _facet, bytes4[] calldata _functionSelectors) ;`} + + +**Parameters:** + + + +--- +### diamondCut + +Add/replace/remove any number of functions and optionally execute a function with delegatecall + + +{`function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) ;`} + + +**Parameters:** + + + +--- +### getStorage + + +{`function getStorage() pure returns (DiamondStorage storage s);`} + + +--- +### removeFunctions + + +{`function removeFunctions(address _facet, bytes4[] calldata _functionSelectors) ;`} + + +**Parameters:** + + + +--- +### replaceFunctions + + +{`function replaceFunctions(address _facet, bytes4[] calldata _functionSelectors) ;`} + + +**Parameters:** + + + +## Events + + + + +
+ Signature: + +{`event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotRemoveFunctionThatDoesNotExist(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotRemoveImmutableFunction(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceFunctionThatDoesNotExists(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceFunctionWithTheSameFunctionFromTheSameFacet(bytes4 _selector); + +
+
+ + +
+ Signature: + +error CannotReplaceImmutableFunction(bytes4 _selector); + +
+
+ + +
+ Signature: + +error IncorrectFacetCutAction(uint8 _action); + +
+
+ + +
+ Signature: + +error InitializationFunctionReverted(address _initializationContractAddress, bytes _calldata); + +
+
+ + +
+ Signature: + +error NoBytecodeAtAddress(address _contractAddress, string _message); + +
+
+ + +
+ Signature: + +error NoSelectorsProvidedForFacet(address _facet); + +
+
+ + +
+ Signature: + +error RemoveFacetAddressMustBeZeroAddress(address _facet); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondCut} from "@compose/diamond-proxy/contracts/modules/diamondCut/IDiamondCut.sol"; +import {Selectors} from "@compose/diamond-proxy/contracts/utils/Selectors.sol"; + +contract MyFacet { + // Facet implementation details... +} + +contract DiamondDeployer { + IDiamondCut public diamondCutFacet; + + function upgradeDiamond(address _diamondAddress) external { + // Assuming diamondCutFacet is already deployed and accessible + diamondCutFacet = IDiamondCut(_diamondAddress); + + // Example: Add a new function + address newFacetAddress = address(new MyFacet()); + bytes4[] memory selectorsToAdd = new bytes4[](1); + selectorsToAdd[0] = MyFacet.myNewFunction.selector; + + // Example: Remove a function + bytes4[] memory selectorsToRemove = new bytes4[](1); + selectorsToRemove[0] = MyFacet.oldFunction.selector; + + // Execute the diamond cut + diamondCutFacet.diamondCut( + IDiamondCut.FacetCut[](\(IDiamondCut.FacetCut[]) + (new IDiamondCut.FacetCut[](2)) + ), + newFacetAddress, // Target address for adding/replacing functions + bytes("") // Initialization calldata + ); + + // Note: For a real upgrade, you would construct the FacetCut array correctly + // with actions (ADD, REPLACE, REMOVE), facet addresses, and selectors. + } +}`} + + +## Best Practices + + +- Use custom errors for revert conditions to enhance clarity and gas efficiency. +- Ensure facet addresses used in `diamondCut` are valid and the associated bytecode is accessible. +- Carefully manage facet additions and removals to avoid breaking existing diamond functionality; consider immutability constraints. + + +## Integration Notes + + +The DiamondCut module directly interacts with the diamond's storage to manage the mapping of selectors to facet addresses. Any changes made through `diamondCut` are immediately reflected in the diamond proxy's dispatch logic. Facets added or replaced become callable via the diamond proxy. Facets removed become inaccessible. The order of operations within a single `diamondCut` call is important for preventing accidental removal of functions that are about to be added or replaced from the same address. + + +
+ +
+ + diff --git a/website/docs/library/diamond/DiamondInspectFacet.mdx b/website/docs/library/diamond/DiamondInspectFacet.mdx new file mode 100644 index 00000000..b0501b48 --- /dev/null +++ b/website/docs/library/diamond/DiamondInspectFacet.mdx @@ -0,0 +1,156 @@ +--- +sidebar_position: 99 +title: "DiamondInspectFacet" +description: "Inspects diamond storage and function mappings." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/DiamondInspectFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Inspects diamond storage and function mappings. + + + +- Provides direct access to the diamond's storage slot via inline assembly. +- Exposes a mapping of all registered function selectors to their respective facet addresses. +- Enables off-chain tooling and on-chain contract logic to understand diamond composition. + + +## Overview + +The DiamondInspectFacet provides read-only access to the diamond's internal state and function routing information. It allows developers to query the raw storage layout and map function selectors to their implementing facets, aiding in debugging and understanding the diamond's composition. + +--- + +## Storage + +### FacetAndPosition + + +{`struct FacetAndPosition { + address facet; + uint32 position; +}`} + + +--- +### DiamondStorage + + +{`struct DiamondStorage { + mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition; + /** + * Array of all function selectors that can be called in the diamond. + */ + bytes4[] selectors; +}`} + + +--- +### FunctionFacetPair + + +{`struct FunctionFacetPair { + bytes4 selector; + address facet; +}`} + + +### State Variables + + + +## Functions + +### functionFacetPairs + +Returns an array of all function selectors and their corresponding facet addresses. Iterates through the diamond's stored selectors and pairs each with its facet. + + +{`function functionFacetPairs() external view returns (FunctionFacetPair[] memory pairs);`} + + +**Returns:** + + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondInspectFacet} from "@compose/contracts/facets/DiamondInspect/IDiamondInspectFacet.sol"; + +contract DiamondInspectUser { + address immutable DIAMOND_PROXY; + + constructor(address diamondProxyAddress) { + DIAMOND_PROXY = diamondProxyAddress; + } + + function inspectDiamond() external view returns (bytes[] memory, (address, address)[]) { + IDiamondInspectFacet diamondInspect = IDiamondInspectFacet(DIAMOND_PROXY); + + // Retrieve the raw storage layout (requires custom ABI encoding or knowledge of storage structure) + // For demonstration, assume a method to decode storage is available externally or via another facet. + // bytes[] memory storageLayout = diamondInspect.getStorage(); // Note: getStorage returns raw bytes, decoding is external. + + // Retrieve function selector to facet address mappings + (address, address)[] memory functionFacetPairs = diamondInspect.functionFacetPairs(); + + return (new bytes[](0), functionFacetPairs); // Placeholder for storageLayout + } +}`} + + +## Best Practices + + +- Integrate this facet to facilitate on-chain inspection of diamond state and function routing. +- Use `functionFacetPairs` to verify function ownership and routing during upgrades or debugging. +- Be aware that `getStorage` returns raw bytes; interpretation requires knowledge of the diamond's storage layout. + + +## Security Considerations + + +This facet is read-only and does not modify state. However, exposing raw storage could reveal sensitive internal data if not properly managed by other facets. Ensure that sensitive data is not stored in an easily accessible manner if this facet is exposed. + + +
+ +
+ + diff --git a/website/docs/library/diamond/DiamondLoupeFacet.mdx b/website/docs/library/diamond/DiamondLoupeFacet.mdx new file mode 100644 index 00000000..08a08f8d --- /dev/null +++ b/website/docs/library/diamond/DiamondLoupeFacet.mdx @@ -0,0 +1,247 @@ +--- +sidebar_position: 99 +title: "DiamondLoupeFacet" +description: "Inspect diamond facets, addresses, and selectors." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/DiamondLoupeFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Inspect diamond facets, addresses, and selectors. + + + +- Provides read-only access to diamond's facet registry. +- Optimized for gas efficiency, especially with a large number of facets and selectors. +- Enables dynamic discovery of diamond functionality. + + +## Overview + +The DiamondLoupeFacet provides essential introspection capabilities for a Compose diamond. It allows developers to query which facets are registered, their associated addresses, and the function selectors each facet implements. This is crucial for understanding diamond composition, debugging, and dynamic interaction. + +--- + +## Storage + +### FacetAndPosition + + +{`struct FacetAndPosition { + address facet; + uint32 position; +}`} + + +--- +### DiamondStorage + + +{`struct DiamondStorage { + mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition; + /** + * Array of all function selectors that can be called in the diamond. + */ + bytes4[] selectors; +}`} + + +--- +### Facet + + +{`struct Facet { + address facet; + bytes4[] functionSelectors; +}`} + + +### State Variables + + + +## Functions + +### facetAddress + +Gets the facet address that supports the given selector. If facet is not found return address(0). + + +{`function facetAddress(bytes4 _functionSelector) external view returns (address facet);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### facetFunctionSelectors + +Gets all the function selectors supported by a specific facet. Returns the set of selectors that this diamond currently routes to the given facet address. How it works: 1. Iterates through the diamond’s global selector list (s.selectors) — i.e., the selectors that have been added to this diamond. 2. For each selector, reads its facet address from diamond storage (s.facetAndPosition[selector].facet) and compares it to `_facet`. 3. When it matches, writes the selector into a preallocated memory array and increments a running count. 4. After the scan, updates the logical length of the result array with assembly to the exact number of matches. Why this approach: - Single-pass O(n) scan over all selectors keeps the logic simple and predictable. - Preallocating to the maximum possible size (total selector count) avoids repeated reallocations while building the result. - Trimming the array length at the end yields an exactly sized return value. + + +{`function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetSelectors);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### facetAddresses + +Get all the facet addresses used by a diamond. This function returns the unique set of facet addresses that provide functionality to the diamond. How it works:** 1. Uses a memory-based hash map to group facet addresses by the last byte of the address, reducing linear search costs from O(n²) to approximately O(n) for most cases. 2. Reuses the selectors array memory space to store the unique facet addresses, avoiding an extra memory allocation for the intermediate array. The selectors array is overwritten with facet addresses as we iterate. 3. For each selector, looks up its facet address and checks if we've seen this address before by searching the appropriate hash map bucket. 4. If the facet is new (not found in the bucket), expands the bucket by 4 slots if it's full or empty, then adds the facet to both the bucket and the return array. 5. If the facet was already seen, skips it to maintain uniqueness. 6. Finally, sets the correct length of the return array to match the number of unique facets found. Why this approach:** - Hash mapping by last address byte provides O(1) average-case bucket lookup instead of scanning all previously-found facets linearly for each selector. - Growing in fixed-size chunks (4 for buckets) keeps reallocations infrequent and prevents over-allocation, while keeping bucket sizes small for sparse key distributions. - Reusing the selectors array memory eliminates one memory allocation and reduces total memory usage, which saves gas. - This design is optimized for diamonds with many selectors across many facets, where the original O(n²) nested loop approach becomes prohibitively expensive. - The 256-bucket hash map trades a small fixed memory cost for dramatic algorithmic improvement in worst-case scenarios. + + +{`function facetAddresses() external view returns (address[] memory allFacets);`} + + +**Returns:** + + + +--- +### facets + +Gets all facets and their selectors. Returns each unique facet address currently used by the diamond and the list of function selectors that the diamond maps to that facet. How it works:** 1. Uses a memory-based hash map to group facets by the last byte of their address, reducing linear search costs from O(n²) to approximately O(n) for most cases. 2. Reuses the selectors array memory space to store pointers to Facet structs, avoiding an extra memory allocation for the intermediate array. 3. For each selector, looks up its facet address and checks if we've seen this facet before by searching the appropriate hash map bucket. 4. If the facet is new, expands the bucket by 4 slots if it's full or empty, creates a Facet struct with a 16-slot selector array, and stores a pointer to it in both the bucket and the facet pointers array. 5. If the facet exists, expands its selector array by 16 slots if full, then appends the selector to the array. 6. Finally, copies all Facet structs from their pointers into a properly-sized return array. Why this approach:** - Hash mapping by last address byte provides O(1) average-case bucket lookup instead of scanning all previously-found facets linearly. - Growing in fixed-size chunks (4 for buckets, 16 for selector arrays) keeps reallocations infrequent and prevents over-allocation. - Reusing the selectors array memory reduces total memory usage and allocation. - This design is optimized for diamonds with many facets and many selectors, where the original O(n²) nested loop approach becomes prohibitively expensive. + + +{`function facets() external view returns (Facet[] memory facetsAndSelectors);`} + + +**Returns:** + + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondLoupe} from "@compose-protocol/diamond-interface/src/facets/IDiamondLoupe.sol"; + +contract DiamondLoupeConsumer { + IDiamondLoupe diamondLoupeFacet; + + constructor(address _diamondAddress) { + diamondLoupeFacet = IDiamondLoupe(_diamondAddress); + } + + function getFacetAddresses() public view returns (address[] memory) { + return diamondLoupeFacet.facetAddresses(); + } + + function getFacetSelectors(address _facet) public view returns (bytes4[] memory) { + return diamondLoupeFacet.facetFunctionSelectors(_facet); + } + + function getAllFacets() public view returns (IDiamondLoupe.Facet[] memory) { + return diamondLoupeFacet.facets(); + } +}`} + + +## Best Practices + + +- Use `facetAddresses()` to retrieve all unique facet addresses registered with the diamond. +- Employ `facetFunctionSelectors(address)` to determine the specific functions implemented by a given facet. +- Utilize `facets()` for a comprehensive view of all registered facets and their corresponding selectors. + + +## Security Considerations + + +This facet is read-only and does not manage state changes, thus posing minimal direct security risks. Ensure the diamond proxy itself is properly secured to prevent unauthorized facet additions or removals. + + +
+ +
+ + diff --git a/website/docs/library/diamond/DiamondMod.mdx b/website/docs/library/diamond/DiamondMod.mdx new file mode 100644 index 00000000..5f5a0dce --- /dev/null +++ b/website/docs/library/diamond/DiamondMod.mdx @@ -0,0 +1,233 @@ +--- +sidebar_position: 99 +title: "DiamondMod" +description: "Manages diamond facets and their function mappings." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/DiamondMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages diamond facets and their function mappings. + + + +- Supports adding multiple facets and their function selectors in a single transaction during deployment. +- Provides the `diamondFallback` mechanism to route calls to the appropriate facet. +- Exposes `getStorage` for introspection of diamond storage layout. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The DiamondMod provides core functionality for managing facets within a Compose diamond. It handles the addition of new facets and their associated function selectors during initial deployment. This module is crucial for composing diamond functionality by mapping external calls to the correct facet implementation. + +--- + +## Storage + +### FacetCutAction + +Add=0, Replace=1, Remove=2 + +--- +### DiamondStorage + +storage-location: erc8042:compose.diamond + + +{`struct DiamondStorage { + mapping(bytes4 functionSelector => FacetAndPosition) facetAndPosition; + /** + * \`selectors\` contains all function selectors that can be called in the diamond. + */ + bytes4[] selectors; +}`} + + +--- +### FacetAndPosition + + +{`struct FacetAndPosition { + address facet; + uint32 position; +}`} + + +--- +### FacetCut + + +{`struct FacetCut { + address facetAddress; + FacetCutAction action; + bytes4[] functionSelectors; +}`} + + +### State Variables + + + +## Functions + +### addFacets + +Adds facets and their function selectors to the diamond. Only supports adding functions during diamond deployment. + + +{`function addFacets(FacetCut[] memory _facets) ;`} + + +**Parameters:** + + + +--- +### diamondFallback + +Find facet for function that is called and execute the function if a facet is found and return any value. + + +{`function diamondFallback() ;`} + + +--- +### getStorage + + +{`function getStorage() pure returns (DiamondStorage storage s);`} + + +## Events + + + + +
+ Signature: + +{`event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error CannotAddFunctionToDiamondThatAlreadyExists(bytes4 _selector); + +
+
+ + +
+ Signature: + +error FunctionNotFound(bytes4 _selector); + +
+
+ + +
+ Signature: + +error InvalidActionWhenDeployingDiamond(address facetAddress, FacetCutAction action, bytes4[] functionSelectors); + +
+
+ + +
+ Signature: + +error NoBytecodeAtAddress(address _contractAddress, string _message); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondMod} from "@compose/diamond-proxy/contracts/modules/DiamondMod.sol"; + +contract MyFacet { + // Facet implementation details... +} + +contract Deployer { + // Assuming diamondMod is an instance of DiamondMod + IDiamondMod internal diamondMod; + + function deployDiamond(bytes[] memory facetBytecodes, bytes32[] memory functionSigs) external { + // ... deployment logic ... + // diamondMod.addFacets(facetBytecodes, functionSigs); // Example call during deployment + } +}`} + + +## Best Practices + + +- Facet additions are restricted to the initial diamond deployment phase to maintain state immutability post-deployment. +- Ensure function selectors and their corresponding facet bytecodes are correctly paired to prevent runtime errors. +- Utilize custom errors for clear and gas-efficient error handling during facet management operations. + + +## Integration Notes + + +The DiamondMod interacts directly with the diamond's storage to register facet implementations and their function selectors. The `addFacets` function is designed to be called only during the initial deployment of the diamond proxy contract. Subsequent modifications to facets or function mappings are not supported by this module post-deployment, enforcing diamond immutability. The `diamondFallback` function relies on the mappings established by `addFacets` to route incoming calls. + + +
+ +
+ + diff --git a/website/docs/library/diamond/_category_.json b/website/docs/library/diamond/_category_.json new file mode 100644 index 00000000..26c8cc37 --- /dev/null +++ b/website/docs/library/diamond/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Diamond Core", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/diamond/index" + } +} diff --git a/website/docs/library/diamond/example/ExampleDiamond.mdx b/website/docs/library/diamond/example/ExampleDiamond.mdx new file mode 100644 index 00000000..ef59906f --- /dev/null +++ b/website/docs/library/diamond/example/ExampleDiamond.mdx @@ -0,0 +1,139 @@ +--- +sidebar_position: 99 +title: "ExampleDiamond" +description: "Example Diamond for Compose framework" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/diamond/example/ExampleDiamond.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Example Diamond for Compose framework + + + +- Demonstrates diamond proxy initialization. +- Supports adding and registering facets with their selectors. +- Establishes contract ownership for management. + + +## Overview + +The ExampleDiamond contract serves as a foundational template within the Compose framework. It demonstrates the core diamond proxy pattern, enabling the registration and routing of functions across various facets. This contract is essential for understanding how to initialize and manage facets within a diamond. + +--- + +## Storage + +## Functions + +### constructor + +Struct to hold facet address and its function selectors. struct FacetCut { address facetAddress; FacetCutAction action; // Add=0, Replace=1, Remove=2 bytes4[] functionSelectors; } Initializes the diamond contract with facets, owner and other data. Adds all provided facets to the diamond's function selector mapping and sets the contract owner. Each facet in the array will have its function selectors registered to enable delegatecall routing. + + +{`constructor(DiamondMod.FacetCut[] memory _facets, address _diamondOwner) ;`} + + +**Parameters:** + + + +--- +### fallback + + +{`fallback() external payable;`} + + +--- +### receive + + +{`receive() external payable;`} + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {DiamondCutFacet} from "@compose/diamond/facets/DiamondCutFacet.sol"; +import {DiamondLoupeFacet} from "@compose/diamond/facets/DiamondLoupeFacet.sol"; +import {ExampleDiamond} from "@compose/diamond/ExampleDiamond.sol"; + +contract DeployExampleDiamond is DiamondCutFacet { + address public diamondProxy; + + function deploy() external { + // Initialize facets array + FacetCut[] memory facets = new FacetCut[](2); + + // Add DiamondCutFacet + facets[0] = FacetCut({ + facetAddress: address(new DiamondCutFacet()), + action: FacetCutAction.Add, + functionSelectors: DiamondCutFacet.getFunctionSelectors() + }); + + // Add DiamondLoupeFacet + facets[1] = FacetCut({ + facetAddress: address(new DiamondLoupeFacet()), + action: FacetCutAction.Add, + functionSelectors: DiamondLoupeFacet.getFunctionSelectors() + }); + + // Deploy ExampleDiamond and initialize it + ExampleDiamond exampleDiamond = new ExampleDiamond(); + exampleDiamond.init(facets, msg.sender); + diamondProxy = address(exampleDiamond); + } +}`} + + +## Best Practices + + +- Initialize the diamond with essential facets like DiamondCutFacet and DiamondLoupeFacet. +- Ensure the owner is set correctly during initialization to manage future upgrades. +- Register function selectors accurately for each facet to enable proper routing. + + +## Security Considerations + + +The constructor initializes the diamond and sets the owner. Ensure the owner address is a trusted entity. Function selectors must be correctly mapped to facet addresses to prevent unexpected behavior or denial of service. + + +
+ +
+ + diff --git a/website/docs/library/diamond/example/_category_.json b/website/docs/library/diamond/example/_category_.json new file mode 100644 index 00000000..8e4d0ed5 --- /dev/null +++ b/website/docs/library/diamond/example/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "example", + "position": 99, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/diamond/example/index" + } +} diff --git a/website/docs/library/diamond/example/index.mdx b/website/docs/library/diamond/example/index.mdx new file mode 100644 index 00000000..4513c4b1 --- /dev/null +++ b/website/docs/library/diamond/example/index.mdx @@ -0,0 +1,23 @@ +--- +title: "example" +description: "example components for Compose diamonds." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + example components for Compose diamonds. + + + + } + size="medium" + /> + diff --git a/website/docs/library/diamond/index.mdx b/website/docs/library/diamond/index.mdx new file mode 100644 index 00000000..7285988b --- /dev/null +++ b/website/docs/library/diamond/index.mdx @@ -0,0 +1,58 @@ +--- +title: "Diamond Core" +description: "Core diamond proxy functionality for ERC-2535 diamonds." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Core diamond proxy functionality for ERC-2535 diamonds. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/index.mdx b/website/docs/library/index.mdx new file mode 100644 index 00000000..8de81297 --- /dev/null +++ b/website/docs/library/index.mdx @@ -0,0 +1,51 @@ +--- +title: "Library" +description: "API reference for all Compose modules and facets." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + API reference for all Compose modules and facets. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/interfaceDetection/ERC165/ERC165Facet.mdx b/website/docs/library/interfaceDetection/ERC165/ERC165Facet.mdx new file mode 100644 index 00000000..6a6d54f9 --- /dev/null +++ b/website/docs/library/interfaceDetection/ERC165/ERC165Facet.mdx @@ -0,0 +1,152 @@ +--- +sidebar_position: 99 +title: "ERC165Facet" +description: "Implements ERC-165 interface detection for diamond contracts." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/interfaceDetection/ERC165/ERC165Facet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Implements ERC-165 interface detection for diamond contracts. + + + +- Implements the `supportsInterface` function as required by ERC-165. +- Utilizes inline assembly for efficient access to ERC-165 storage. + + +## Overview + +The ERC165Facet enables diamond contracts to comply with the ERC-165 standard, allowing external contracts to query which interfaces the diamond supports. This facet is crucial for discoverability and interoperability within the Ethereum ecosystem. + +--- + +## Storage + +### ERC165Storage + + +{`struct ERC165Storage { + /** + * @notice Mapping of interface IDs to whether they are supported + */ + mapping(bytes4 => bool) supportedInterfaces; +}`} + + +### State Variables + + + +## Functions + +### supportsInterface + +Query if a contract implements an interface This function checks if the diamond supports the given interface ID + + +{`function supportsInterface(bytes4 _interfaceId) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {ERC165Facet} from "@compose/contracts/src/facets/ERC165Facet.sol"; +import {IDiamondCut} from "@compose/contracts/src/interfaces/IDiamondCut.sol"; + +contract MyDiamond is IDiamondCut { + // ... other facet deployments and cuts ... + + function upgrade() external payable { + // ... other facet cuts ... + + address[] memory facetsToAddAddresses = new address[](1); + bytes[] memory facetToAddABIs = new bytes[](1); + + facetsToAddAddresses[0] = address(new ERC165Facet()); + facetToAddABIs[0] = abi.encodeCall(ERC165Facet.supportsInterface, (bytes4(keccak256("supportsInterface(bytes4)")))); + + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1); + cuts[0] = IDiamondCut.FacetCut({ + facetAddress: facetsToAddAddresses[0], + action: IDiamondCut.FacetCutAction.ADD, + isConstructor: false, + functionSelectors: facetToAddABIs[0] + }); + + diamondCut(cuts, address(0), ""); + } + + // ... other diamond functions ... +}`} + + +## Best Practices + + +- Ensure the ERC165Facet is added to the diamond during initial deployment or upgrades. +- Correctly populate the diamond's storage with supported interface IDs to be queried by `supportsInterface`. + + +## Security Considerations + + +This facet is read-only and does not directly handle asset transfers or critical state changes, minimizing reentrancy risks. Ensure that the interface IDs registered in the diamond's storage accurately reflect the implemented functionalities. + + +
+ +
+ + diff --git a/website/docs/library/interfaceDetection/ERC165/ERC165Mod.mdx b/website/docs/library/interfaceDetection/ERC165/ERC165Mod.mdx new file mode 100644 index 00000000..54a7fdea --- /dev/null +++ b/website/docs/library/interfaceDetection/ERC165/ERC165Mod.mdx @@ -0,0 +1,154 @@ +--- +sidebar_position: 99 +title: "ERC165Mod" +description: "ERC-165 interface detection and registration." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/interfaceDetection/ERC165/ERC165Mod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +ERC-165 interface detection and registration. + + + +- Implements ERC-165 standard for interface detection. +- Allows registration of supported interfaces at the facet level. +- Minimal storage footprint, designed for composition within the diamond storage pattern. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The ERC165Mod provides the necessary storage and logic for implementing the ERC-165 standard on a diamond proxy. It allows facets to register supported interfaces, enabling external contracts to query the diamond's capabilities through the `supportsInterface` function. + +--- + +## Storage + +### ERC165Storage + + +{`struct ERC165Storage { + /* + * @notice Mapping of interface IDs to whether they are supported + */ + mapping(bytes4 => bool) supportedInterfaces; +}`} + + +### State Variables + + + +## Functions + +### getStorage + +Returns a pointer to the ERC-165 storage struct. Uses inline assembly to bind the storage struct to the fixed storage position. + + +{`function getStorage() pure returns (ERC165Storage storage s);`} + + +**Returns:** + + + +--- +### registerInterface + +Register that a contract supports an interface Call this function during initialization to register supported interfaces. For example, in an ERC721 facet initialization, you would call: `LibERC165.registerInterface(type(IERC721).interfaceId)` + + +{`function registerInterface(bytes4 _interfaceId) ;`} + + +**Parameters:** + + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {ERC165Mod, IERC165Mod} from "@compose-protocol/diamond-contracts/contracts/modules/ERC165Mod.sol"; + +contract MyERC721Facet { + struct MyFacetStorage { + ERC165Mod.Storage erc165Storage; + // other facet storage... + } + + function initialize(MyFacetStorage storage self) external { + // Register ERC721 interface + ERC165Mod.registerInterface(self.erc165Storage, type(IERC721).interfaceId); + } + + // Other ERC721 functions... +}`} + + +## Best Practices + + +- Call `registerInterface` during facet initialization to ensure supported interfaces are declared before any external calls are made. +- Ensure the `ERC165Mod.Storage` struct is correctly laid out in your facet's storage and is initialized. +- Rely on the diamond's `supportsInterface` function, which aggregates results from all registered facets. + + +## Integration Notes + + +The ERC165Mod utilizes a dedicated storage slot for its `Storage` struct. Facets that implement ERC-165 functionality must include this struct in their own storage layout and call `ERC165Mod.registerInterface` during their initialization. The diamond's `supportsInterface` function aggregates the interface IDs registered by all facets to provide a comprehensive answer. + + +
+ +
+ + diff --git a/website/docs/library/interfaceDetection/ERC165/_category_.json b/website/docs/library/interfaceDetection/ERC165/_category_.json new file mode 100644 index 00000000..2396f18a --- /dev/null +++ b/website/docs/library/interfaceDetection/ERC165/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-165", + "position": 99, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/interfaceDetection/ERC165/index" + } +} diff --git a/website/docs/library/interfaceDetection/ERC165/index.mdx b/website/docs/library/interfaceDetection/ERC165/index.mdx new file mode 100644 index 00000000..8de71f9d --- /dev/null +++ b/website/docs/library/interfaceDetection/ERC165/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-165" +description: "ERC-165 components for Compose diamonds." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-165 components for Compose diamonds. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/interfaceDetection/_category_.json b/website/docs/library/interfaceDetection/_category_.json new file mode 100644 index 00000000..a184d836 --- /dev/null +++ b/website/docs/library/interfaceDetection/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Interface Detection", + "position": 5, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/interfaceDetection/index" + } +} diff --git a/website/docs/library/interfaceDetection/index.mdx b/website/docs/library/interfaceDetection/index.mdx new file mode 100644 index 00000000..17feecdd --- /dev/null +++ b/website/docs/library/interfaceDetection/index.mdx @@ -0,0 +1,23 @@ +--- +title: "Interface Detection" +description: "ERC-165 interface detection support." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-165 interface detection support. + + + + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC1155/ERC1155Facet.mdx b/website/docs/library/token/ERC1155/ERC1155Facet.mdx new file mode 100644 index 00000000..ccfdce70 --- /dev/null +++ b/website/docs/library/token/ERC1155/ERC1155Facet.mdx @@ -0,0 +1,653 @@ +--- +sidebar_position: 99 +title: "ERC1155Facet" +description: "Implements the ERC-1155 multi-token standard." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC1155/ERC1155Facet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Implements the ERC-1155 multi-token standard. + + + +- Implements ERC-1155 standard functions: `balanceOf`, `balanceOfBatch`, `uri`, `safeTransferFrom`, `safeBatchTransferFrom`, `setApprovalForAll`, `isApprovedForAll`. +- Supports token-specific URIs in addition to a base URI. +- Utilizes inline assembly for efficient access to diamond storage. + + +## Overview + +This facet provides a robust implementation of the ERC-1155 multi-token standard, enabling the management and transfer of fungible and non-fungible tokens within a Compose diamond. It handles token balances, approvals, and URI retrieval, adhering to the standard's specifications for composability. + +--- + +## Storage + +### ERC1155Storage + + +{`struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; +}`} + + +### State Variables + + + +## Functions + +### uri + +Returns the URI for token type `_id`. If a token-specific URI is set in tokenURIs[_id], returns the concatenation of baseURI and tokenURIs[_id]. Note that baseURI is empty by default and must be set explicitly if concatenation is desired. If no token-specific URI is set, returns the default URI which applies to all token types. The default URI may contain the substring `{id}` which clients should replace with the actual token ID. + + +{`function uri(uint256 _id) external view returns (string memory);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### balanceOf + +Returns the amount of tokens of token type `id` owned by `account`. + + +{`function balanceOf(address _account, uint256 _id) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### balanceOfBatch + +Batched version of balanceOf. + + +{`function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) + external + view + returns (uint256[] memory balances);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### setApprovalForAll + +Grants or revokes permission to `operator` to transfer the caller's tokens. Emits an ApprovalForAll event. + + +{`function setApprovalForAll(address _operator, bool _approved) external;`} + + +**Parameters:** + + + +--- +### isApprovedForAll + +Returns true if `operator` is approved to transfer `account`'s tokens. + + +{`function isApprovedForAll(address _account, address _operator) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### safeTransferFrom + +Transfers `value` amount of token type `id` from `from` to `to`. Emits a TransferSingle event. + + +{`function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;`} + + +**Parameters:** + + + +--- +### safeBatchTransferFrom + +Batched version of safeTransferFrom. Emits a TransferBatch event. + + +{`function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data +) external;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. +
+ +
+ Signature: + +{`event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Equivalent to multiple TransferSingle events, where `operator`, `from` and `to` are the same for all transfers. +
+ +
+ Signature: + +{`event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. +
+ +
+ Signature: + +{`event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when the URI for token type `id` changes to `value`. +
+ +
+ Signature: + +{`event URI(string _value, uint256 indexed _id);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Error indicating insufficient balance for a transfer. +
+ +
+ Signature: + +error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + +
+
+ +
+ Error indicating the sender address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidSender(address _sender); + +
+
+ +
+ Error indicating the receiver address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidReceiver(address _receiver); + +
+
+ +
+ Error indicating missing approval for an operator. +
+ +
+ Signature: + +error ERC1155MissingApprovalForAll(address _operator, address _owner); + +
+
+ +
+ Error indicating the approver address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidApprover(address _approver); + +
+
+ +
+ Error indicating the operator address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidOperator(address _operator); + +
+
+ +
+ Error indicating array length mismatch in batch operations. +
+ +
+ Signature: + +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; + +// Assume Diamond ABI and implementation contracts are available +// For example, ERC1155Facet contract is deployed and its ABI is known + +contract DeployERC1155 { + address public diamondAddress; + + function deploy(address _diamondAddress) external { + diamondAddress = _diamondAddress; + } + + function getERC1155Facet() internal view returns (IERC1155) { + // Selector for ERC1155Facet.safeTransferFrom + bytes4 selector = bytes4(keccak256("safeTransferFrom(address,address,uint256,uint256,bytes)")); + return IERC1155(address(uint160(address(diamondAddress)) - selector)); + } + + function transferToken(address _from, address _to, uint256 _id, uint256 _value) external { + IERC1155 erc1155Facet = getERC1155Facet(); + erc1155Facet.safeTransferFrom(_from, _to, _id, _value, ""); + } + + function getBalance(address _account, uint256 _id) external view returns (uint256) { + IERC1155 erc1155Facet = getERC1155Facet(); + return erc1155Facet.balanceOf(_account, _id); + } +}`} + + +## Best Practices + + +- Initialize the `baseURI` and `tokenURIs` storage variables via an initializer function or a separate facet if needed. +- Use `safeTransferFrom` and `safeBatchTransferFrom` for all token transfers to ensure proper checks and event emissions. +- Manage approvals using `setApprovalForAll` before allowing operators to transfer tokens on behalf of an account. + + +## Security Considerations + + +Ensure that the `safeTransferFrom` and `safeBatchTransferFrom` functions are called with valid `from`, `to`, `id`, and `value` parameters to prevent unexpected behavior. The `ERC1155MissingApprovalForAll` and `ERC1155InvalidOperator` errors are crucial for preventing unauthorized transfers. Reentrancy is mitigated as transfers are internal and do not involve external calls to untrusted contracts within the transfer logic itself. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC1155/ERC1155Mod.mdx b/website/docs/library/token/ERC1155/ERC1155Mod.mdx new file mode 100644 index 00000000..8d6f78bb --- /dev/null +++ b/website/docs/library/token/ERC1155/ERC1155Mod.mdx @@ -0,0 +1,601 @@ +--- +sidebar_position: 99 +title: "ERC1155Mod" +description: "Manages ERC-1155 token transfers, minting, and burning." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC1155/ERC1155Mod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages ERC-1155 token transfers, minting, and burning. + + + +- Supports both single and batch operations for transfers, minting, and burning. +- Includes `safeTransferFrom` and `safeBatchTransferFrom` for secure transfers to ERC-1155 compliant receivers. +- Provides functions to set and retrieve token URIs, enabling metadata management. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The ERC1155Mod provides comprehensive functionality for managing ERC-1155 tokens within a Compose diamond. It enables minting, burning, and safe transfers of both single and batch token types, adhering to EIP-1155 standards. This module ensures proper handling of token balances and receiver interactions, crucial for composable NFT and fungible token systems. + +--- + +## Storage + +### ERC1155Storage + +ERC-8042 compliant storage struct for ERC-1155 token data. storage-location: erc8042:compose.erc1155 + + +{`struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns a single token type from an address. Decreases the balance and emits a TransferSingle event. Reverts if the account has insufficient balance. + + +{`function burn(address _from, uint256 _id, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### burnBatch + +Burns multiple token types from an address in a single transaction. Decreases balances for each token type and emits a TransferBatch event. Reverts if the account has insufficient balance for any token type. + + +{`function burnBatch(address _from, uint256[] memory _ids, uint256[] memory _values) ;`} + + +**Parameters:** + + + +--- +### getStorage + +Returns the ERC-1155 storage struct from the predefined diamond storage slot. Uses inline assembly to set the storage slot reference. + + +{`function getStorage() pure returns (ERC1155Storage storage s);`} + + +**Returns:** + + + +--- +### mint + +Mints a single token type to an address. Increases the balance and emits a TransferSingle event. Performs receiver validation if recipient is a contract. + + +{`function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) ;`} + + +**Parameters:** + + + +--- +### mintBatch + +Mints multiple token types to an address in a single transaction. Increases balances for each token type and emits a TransferBatch event. Performs receiver validation if recipient is a contract. + + +{`function mintBatch(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) ;`} + + +**Parameters:** + + + +--- +### safeBatchTransferFrom + +Safely transfers multiple token types from one address to another in a single transaction. Validates ownership, approval, and receiver address before updating balances for each token type. Performs ERC1155Receiver validation if recipient is a contract (safe transfer). Complies with EIP-1155 safe transfer requirements. + + +{`function safeBatchTransferFrom( +address _from, +address _to, +uint256[] memory _ids, +uint256[] memory _values, +address _operator +) ;`} + + +**Parameters:** + + + +--- +### safeTransferFrom + +Safely transfers a single token type from one address to another. Validates ownership, approval, and receiver address before updating balances. Performs ERC1155Receiver validation if recipient is a contract (safe transfer). Complies with EIP-1155 safe transfer requirements. + + +{`function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, address _operator) ;`} + + +**Parameters:** + + + +--- +### setBaseURI + +Sets the base URI prefix for token-specific URIs. The base URI is concatenated with token-specific URIs set via setTokenURI. Does not affect the default URI used when no token-specific URI is set. + + +{`function setBaseURI(string memory _baseURI) ;`} + + +**Parameters:** + + + +--- +### setTokenURI + +Sets the token-specific URI for a given token ID. Sets tokenURIs[_tokenId] to the provided string and emits a URI event with the full computed URI. The emitted URI is the concatenation of baseURI and the token-specific URI. + + +{`function setTokenURI(uint256 _tokenId, string memory _tokenURI) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when multiple token types are transferred. +
+ +
+ Signature: + +{`event TransferBatch( +address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a single token type is transferred. +
+ +
+ Signature: + +{`event TransferSingle( +address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when the URI for token type `_id` changes to `_value`. +
+ +
+ Signature: + +{`event URI(string _value, uint256 indexed _id);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ **Title:** LibERC1155 — ERC-1155 Library Provides internal functions and storage layout for ERC-1155 multi-token logic. Thrown when insufficient balance for a transfer or burn operation. Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. This library is intended to be used by custom facets to integrate with ERC-1155 functionality. +
+ +
+ Signature: + +error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + +
+
+ +
+ Thrown when array lengths don't match in batch operations. +
+ +
+ Signature: + +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +
+
+ +
+ Thrown when the receiver address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid. +
+ +
+ Signature: + +error ERC1155InvalidSender(address _sender); + +
+
+ +
+ Thrown when missing approval for an operator. +
+ +
+ Signature: + +error ERC1155MissingApprovalForAll(address _operator, address _owner); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC1155Mod } from "@compose-protocol/diamond/facets/ERC1155/ERC1155Mod.sol"; + +contract MyFacet { + address immutable diamondProxy; + + constructor(address _diamondProxy) { + diamondProxy = _diamondProxy; + } + + function mintTokens(address _to, uint256 _id, uint256 _amount) external { + IERC1155Mod(diamondProxy).mint(_to, _id, _amount); + } + + function transferTokens(address _from, address _to, uint256 _id, uint256 _amount) external { + IERC1155Mod(diamondProxy).safeTransferFrom(_from, _to, _id, _amount, ""); + } +}`} + + +## Best Practices + + +- Implement robust access control for minting and burning functions if required by your diamond's architecture. +- Ensure proper validation of receiver addresses, especially when interacting with other contracts, by implementing ERC1155Receiver logic. +- Always check balances before attempting to transfer or burn tokens to prevent `ERC1155InsufficientBalance` errors. + + +## Integration Notes + + +The ERC1155Mod utilizes a predefined storage slot within the diamond's storage layout to manage ERC-1155 token balances, approvals, and URI information. Facets interacting with this module should call its functions through the diamond proxy. The `getStorage` function can be used to access the underlying storage struct for read-only operations or for advanced integration, provided the caller understands the storage layout and potential for concurrent modification. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC1155/_category_.json b/website/docs/library/token/ERC1155/_category_.json new file mode 100644 index 00000000..cdb57d9a --- /dev/null +++ b/website/docs/library/token/ERC1155/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-1155", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC1155/index" + } +} diff --git a/website/docs/library/token/ERC1155/index.mdx b/website/docs/library/token/ERC1155/index.mdx new file mode 100644 index 00000000..24f5b890 --- /dev/null +++ b/website/docs/library/token/ERC1155/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-1155" +description: "ERC-1155 multi-token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-1155 multi-token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/ERC20/ERC20BurnFacet.mdx b/website/docs/library/token/ERC20/ERC20/ERC20BurnFacet.mdx new file mode 100644 index 00000000..385a6ffd --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20/ERC20BurnFacet.mdx @@ -0,0 +1,232 @@ +--- +sidebar_position: 99 +title: "ERC20BurnFacet" +description: "Burn ERC-20 tokens within a Compose diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20/ERC20BurnFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Burn ERC-20 tokens within a Compose diamond. + + + +- Allows burning of ERC-20 tokens directly from the diamond. +- Supports burning from the caller's balance (`burn`). +- Supports burning from another account's balance with prior allowance (`burnFrom`). +- Emits standard `Transfer` events to the zero address upon successful burns. + + +## Overview + +The ERC20BurnFacet enables the burning of ERC-20 tokens directly within a Compose diamond. It provides functions to burn tokens from the caller's balance or from another account using allowances, ensuring compliance with the ERC-20 standard by emitting Transfer events to the zero address. + +--- + +## Storage + +### ERC20Storage + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns (destroys) a specific amount of tokens from the caller's balance. Emits a Transfer event to the zero address. + + +{`function burn(uint256 _value) external;`} + + +**Parameters:** + + + +--- +### burnFrom + +Burns tokens from another account, deducting from the caller's allowance. Emits a Transfer event to the zero address. + + +{`function burnFrom(address _account, uint256 _value) external;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when tokens are transferred between two addresses. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when an account has insufficient balance for a transfer or burn. +
+ +
+ Signature: + +error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + +
+
+ +
+ Thrown when a spender tries to use more than the approved allowance. +
+ +
+ Signature: + +error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20BurnFacet} from "@compose/contracts/facets/ERC20/IERC20BurnFacet.sol"; + +contract ERC20BurnConsumer { + address internal diamondAddress; + + constructor(address _diamondAddress) { + diamondAddress = _diamondAddress; + } + + function consumeBurn(address _tokenAddress, uint256 _amount) external { + // Assuming the ERC20BurnFacet is registered for the token address + // Function selector for burn is 0x17b03b88 + (bool success, ) = diamondAddress.call(abi.encodeWithSelector(bytes4(keccak256("burn(address,uint256)")), _tokenAddress, _amount)); + require(success, "Burn failed"); + } + + function consumeBurnFrom(address _tokenAddress, address _from, uint256 _amount) external { + // Function selector for burnFrom is 0x51789f0c + (bool success, ) = diamondAddress.call(abi.encodeWithSelector(bytes4(keccak256("burnFrom(address,address,uint256)")), _tokenAddress, _from, _amount)); + require(success, "BurnFrom failed"); + } +}`} + + +## Best Practices + + +- Ensure the `ERC20BurnFacet` is properly registered in the diamond's facet registry for the relevant ERC-20 token addresses. +- Use `burnFrom` only after an allowance has been set using `ERC20ApproveFacet`. +- Handle potential `ERC20InsufficientBalance` and `ERC20InsufficientAllowance` errors appropriately in consumer contracts. + + +## Security Considerations + + +This facet relies on the underlying ERC-20 token contract's balance and allowance checks. Ensure the `_amount` to be burned does not exceed the caller's balance or allowance, as enforced by the facet's error conditions. No reentrancy concerns are present as the functions do not make external calls after state changes. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20/ERC20Facet.mdx b/website/docs/library/token/ERC20/ERC20/ERC20Facet.mdx new file mode 100644 index 00000000..799b587b --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20/ERC20Facet.mdx @@ -0,0 +1,545 @@ +--- +sidebar_position: 99 +title: "ERC20Facet" +description: "Implements the ERC-20 token standard." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20/ERC20Facet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Implements the ERC-20 token standard. + + + +- Implements the core ERC-20 standard functions. +- Supports token transfers and `transferFrom` with allowance checks. +- Emits standard `Transfer` and `Approval` events. +- Allows querying token metadata and balances. + + +## Overview + +This facet provides a standard ERC-20 token interface for Compose diamonds. It handles token metadata, supply, balances, allowances, and transfers, enabling fungible token functionality within the diamond. + +--- + +## Storage + +### ERC20Storage + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + uint8 decimals; + string name; + string symbol; +}`} + + +### State Variables + + + +## Functions + +### name + +Returns the name of the token. + + +{`function name() external view returns (string memory);`} + + +**Returns:** + + + +--- +### symbol + +Returns the symbol of the token. + + +{`function symbol() external view returns (string memory);`} + + +**Returns:** + + + +--- +### decimals + +Returns the number of decimals used for token precision. + + +{`function decimals() external view returns (uint8);`} + + +**Returns:** + + + +--- +### totalSupply + +Returns the total supply of tokens. + + +{`function totalSupply() external view returns (uint256);`} + + +**Returns:** + + + +--- +### balanceOf + +Returns the balance of a specific account. + + +{`function balanceOf(address _account) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### allowance + +Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. + + +{`function allowance(address _owner, address _spender) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### approve + +Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. Emits an Approval event. + + +{`function approve(address _spender, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### transfer + +Transfers tokens to another address. Emits a Transfer event. + + +{`function transfer(address _to, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### transferFrom + +Transfers tokens on behalf of another account, provided sufficient allowance exists. Emits a Transfer event and decreases the spender's allowance. + + +{`function transferFrom(address _from, address _to, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +## Events + + + +
+ Emitted when an approval is made for a spender by an owner. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when tokens are transferred between two addresses. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when an account has insufficient balance for a transfer or burn. +
+ +
+ Signature: + +error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + +
+
+ +
+ Thrown when the sender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSender(address _sender); + +
+
+ +
+ Thrown when the receiver address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when a spender tries to use more than the approved allowance. +
+ +
+ Signature: + +error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + +
+
+ +
+ Thrown when the spender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSpender(address _spender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20Facet} from "@compose/contracts/src/facets/ERC20/IERC20Facet.sol"; + +contract ERC20Consumer { + IERC20Facet public erc20Facet; + + constructor(address _erc20FacetAddress) { + erc20Facet = IERC20Facet(_erc20FacetAddress); + } + + function getTokenName() public view returns (string memory) { + return erc20Facet.name(); + } + + function checkBalance(address _account) public view returns (uint256) { + return erc20Facet.balanceOf(_account); + } + + function approveSpending(address _spender, uint256 _amount) public { + erc20Facet.approve(_spender, _amount); + } +}`} + + +## Best Practices + + +- Ensure the `ERC20Facet` is initialized with correct token metadata (name, symbol, decimals) during diamond deployment. +- Manage allowances carefully, especially when approving large amounts or indefinite spending. +- Implement access control on functions that modify token supply or ownership if required by your tokenomics. + + +## Security Considerations + + +Standard ERC-20 vulnerabilities apply. Ensure proper input validation for addresses and amounts. Be cautious with `approve` calls to prevent unintended allowance grants. Reentrancy is mitigated by the diamond proxy pattern and the facet's internal logic. Function calls like `transfer` and `transferFrom` should be guarded against the sender having insufficient balance or allowance respectively using the provided custom errors. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20/ERC20Mod.mdx b/website/docs/library/token/ERC20/ERC20/ERC20Mod.mdx new file mode 100644 index 00000000..7e73a4fe --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20/ERC20Mod.mdx @@ -0,0 +1,430 @@ +--- +sidebar_position: 99 +title: "ERC20Mod" +description: "Standard ERC-20 token logic for Compose diamonds." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20/ERC20Mod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Standard ERC-20 token logic for Compose diamonds. + + + +- Implements standard ERC-20 `transfer`, `approve`, `transferFrom`, `mint`, and `burn` functions. +- Manages ERC-20 token balances and allowances through dedicated storage. +- Provides internal helper functions for ERC-20 operations, promoting reusability. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The ERC20Mod provides the core functions and storage layout for implementing the ERC-20 token standard within a Compose diamond. It ensures composability by adhering to standard patterns for token transfers, approvals, minting, and burning, allowing facets to integrate and extend ERC-20 functionality safely. + +--- + +## Storage + +### ERC20Storage + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + uint8 decimals; + string name; + string symbol; +}`} + + +### State Variables + + + +## Functions + +### approve + +Approves a spender to transfer tokens on behalf of the caller. Sets the allowance for the spender. + + +{`function approve(address _spender, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### burn + +Burns tokens from a specified address. Decreases both total supply and the sender's balance. + + +{`function burn(address _account, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### getStorage + +Returns a pointer to the ERC-20 storage struct. Uses inline assembly to bind the storage struct to the fixed storage position. + + +{`function getStorage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- +### mint + +Mints new tokens to a specified address. Increases both total supply and the recipient's balance. + + +{`function mint(address _account, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### transfer + +Transfers tokens from the caller to another address. Updates balances directly without allowance mechanism. + + +{`function transfer(address _to, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### transferFrom + +Transfers tokens from one address to another using an allowance. Deducts the spender's allowance and updates balances. + + +{`function transferFrom(address _from, address _to, uint256 _value) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when an approval is made for a spender by an owner. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when tokens are transferred between addresses. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when a spender tries to spend more than their allowance. +
+ +
+ Signature: + +error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + +
+
+ +
+ Thrown when a sender attempts to transfer or burn more tokens than their balance. +
+ +
+ Signature: + +error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + +
+
+ +
+ Thrown when the receiver address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSender(address _sender); + +
+
+ +
+ Thrown when the spender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSpender(address _spender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20Mod } from "@compose/modules/erc20/ERC20Mod.sol"; + +contract MyERC20Facet { + struct Storage { + ERC20Mod.ERC20Storage erc20; + } + + Storage instance; + + function transferTokens(address to, uint256 amount) external { + instance.erc20.transfer(msg.sender, to, amount); + } + + function approveTokens(address spender, uint256 amount) external { + instance.erc20.approve(msg.sender, spender, amount); + } + + function mintTokens(address recipient, uint256 amount) external { + instance.erc20.mint(recipient, amount); + } + + function burnTokens(address from, uint256 amount) external { + instance.erc20.burn(from, amount); + } + + function getAllowance(address owner, address spender) external view returns (uint256) { + return instance.erc20.allowance(owner, spender); + } +}`} + + +## Best Practices + + +- Ensure the `ERC20Storage` struct is correctly initialized in the diamond's storage layout. +- Always use the provided `transferFrom` function for token movements involving allowances to maintain state integrity. +- Handle custom errors like `ERC20InsufficientBalance` and `ERC20InsufficientAllowance` in your facet logic. + + +## Integration Notes + + +The ERC20Mod uses a fixed storage slot for its `ERC20Storage` struct, accessible via the `getStorage` function. Facets integrating this module must include this struct in their own storage layout and ensure it's properly bound to the correct slot. All ERC-20 state changes (balances, allowances, total supply) are managed internally by the module and are immediately visible to other facets interacting with the diamond. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20/_category_.json b/website/docs/library/token/ERC20/ERC20/_category_.json new file mode 100644 index 00000000..bd8d3da5 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-20", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/ERC20/index" + } +} diff --git a/website/docs/library/token/ERC20/ERC20/index.mdx b/website/docs/library/token/ERC20/ERC20/index.mdx new file mode 100644 index 00000000..d3993e36 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20/index.mdx @@ -0,0 +1,37 @@ +--- +title: "ERC-20" +description: "ERC-20 fungible token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-20 fungible token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.mdx b/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.mdx new file mode 100644 index 00000000..ba968049 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.mdx @@ -0,0 +1,390 @@ +--- +sidebar_position: 99 +title: "ERC20BridgeableFacet" +description: "Manages cross-chain ERC20 token transfers and minting/burning." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20Bridgeable/ERC20BridgeableFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages cross-chain ERC20 token transfers and minting/burning. + + + +- Enables cross-chain minting and burning of ERC20 tokens. +- Restricts `crosschainMint` and `crosschainBurn` functions to addresses with the `trusted-bridge` role. +- Utilizes inline assembly for efficient storage access via the diamond storage pattern. + + +## Overview + +The ERC20BridgeableFacet enables secure cross-chain operations for ERC20 tokens. It allows trusted bridges to mint tokens on one chain and burn them on another. This facet leverages the diamond's storage pattern for efficient access to ERC20 and access control configurations. + +--- + +## Storage + +### ERC20Storage + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +}`} + + +--- +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; +}`} + + +### State Variables + + + +## Functions + +### crosschainMint + +Cross-chain mint — callable only by an address having the `trusted-bridge` role. + + +{`function crosschainMint(address _account, uint256 _value) external;`} + + +**Parameters:** + + + +--- +### crosschainBurn + +Cross-chain burn — callable only by an address having the `trusted-bridge` role. + + +{`function crosschainBurn(address _from, uint256 _value) external;`} + + +**Parameters:** + + + +--- +### checkTokenBridge + +Internal check to check if the bridge (caller) is trusted. Reverts if caller is zero or not in the AccessControl `trusted-bridge` role. + + +{`function checkTokenBridge(address _caller) external view;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when tokens are minted via a cross-chain bridge. +
+ +
+ Signature: + +{`event CrosschainMint(address indexed _to, uint256 _amount, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a crosschain transfer burns tokens. +
+ +
+ Signature: + +{`event CrosschainBurn(address indexed _from, uint256 _amount, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when tokens are transferred between two addresses. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Revert when a provided receiver is invalid(e.g,zero address) . +
+ +
+ Signature: + +error ERC20InvalidReciever(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSender(address _sender); + +
+
+ +
+ Revert when caller is not a trusted bridge. +
+ +
+ Signature: + +error ERC20InvalidBridgeAccount(address _caller); + +
+
+ +
+ Revert when caller address is invalid. +
+ +
+ Signature: + +error ERC20InvalidCallerAddress(address _caller); + +
+
+ +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+ + +
+ Signature: + +error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20BridgeableFacet} from "./interfaces/IERC20BridgeableFacet.sol"; + +contract Deployer { + // Assume diamond is deployed and selectors are registered + address internal immutable diamondAddress; + + constructor(address _diamondAddress) { + diamondAddress = _diamondAddress; + } + + function mintCrosschain(address _token, address _to, uint256 _amount) external { + bytes4 selector = IERC20BridgeableFacet.crosschainMint.selector; + // Call through the diamond proxy + (bool success, ) = diamondAddress.call(abi.encodeWithSelector(selector, _token, _to, _amount)); + require(success, "Crosschain mint failed"); + } + + function burnCrosschain(address _token, address _from, uint256 _amount) external { + bytes4 selector = IERC20BridgeableFacet.crosschainBurn.selector; + // Call through the diamond proxy + (bool success, ) = diamondAddress.call(abi.encodeWithSelector(selector, _token, _from, _amount)); + require(success, "Crosschain burn failed"); + } +}`} + + +## Best Practices + + +- Initialize the `trusted-bridge` role in AccessControl for addresses authorized to call `crosschainMint` and `crosschainBurn`. +- Ensure that the ERC20 token contract is correctly deployed and accessible to the diamond. +- Use `getERC20Storage` and `getAccessControlStorage` to retrieve necessary configuration data. + + +## Security Considerations + + +The `crosschainMint` and `crosschainBurn` functions are protected by the `trusted-bridge` role, preventing unauthorized cross-chain operations. Input validation is performed by internal checks, including verifying the caller's role and ensuring valid recipient/sender addresses. Reentrancy is not a direct concern as these functions do not make external calls to untrusted contracts. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableMod.mdx b/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableMod.mdx new file mode 100644 index 00000000..57d9f71d --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Bridgeable/ERC20BridgeableMod.mdx @@ -0,0 +1,421 @@ +--- +sidebar_position: 99 +title: "ERC20BridgeableMod" +description: "Enables cross-chain token transfers and management." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20Bridgeable/ERC20BridgeableMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Enables cross-chain token transfers and management. + + + +- Cross-chain token minting and burning capabilities. +- Access control for trusted bridge operators. +- Explicit error handling for invalid operations. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The ERC20Bridgeable module facilitates secure cross-chain token operations. It manages trusted bridge addresses and handles the logic for burning and minting tokens across different chains, ensuring controlled and auditable inter-chain asset movements. This module is crucial for applications requiring decentralized cross-chain functionality. + +--- + +## Storage + +### AccessControlStorage + + +{`struct AccessControlStorage { + mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; +}`} + + +--- +### ERC20Storage + +ERC-8042 compliant storage struct for ERC20 token data. storage-location: erc8042:compose.erc20 + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +}`} + + +### State Variables + + + +## Functions + +### checkTokenBridge + +Internal check to check if the bridge (caller) is trusted. Reverts if caller is zero or not in the AccessControl `trusted-bridge` role. + + +{`function checkTokenBridge(address _caller) view;`} + + +**Parameters:** + + + +--- +### crosschainBurn + +Cross-chain burn — callable only by an address having the `trusted-bridge` role. + + +{`function crosschainBurn(address _from, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### crosschainMint + +Cross-chain mint — callable only by an address having the `trusted-bridge` role. + + +{`function crosschainMint(address _account, uint256 _value) ;`} + + +**Parameters:** + + + +--- +### getAccessControlStorage + +helper to return AccessControlStorage at its diamond slot + + +{`function getAccessControlStorage() pure returns (AccessControlStorage storage s);`} + + +--- +### getERC20Storage + +Returns the ERC20 storage struct from the predefined diamond storage slot. Uses inline assembly to set the storage slot reference. + + +{`function getERC20Storage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +## Events + + + +
+ Emitted when a crosschain transfer burns tokens. +
+ +
+ Signature: + +{`event CrosschainBurn(address indexed _from, uint256 _amount, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when tokens are minted via a cross-chain bridge. +
+ +
+ Signature: + +{`event CrosschainMint(address indexed _to, uint256 _amount, address indexed _sender);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when tokens are transferred between two addresses. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the account does not have a specific role. +
+ +
+ Signature: + +error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + +
+
+ + +
+ Signature: + +error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); + +
+
+ +
+ Revert when caller is not a trusted bridge. +
+ +
+ Signature: + +error ERC20InvalidBridgeAccount(address _caller); + +
+
+ +
+ Revert when caller address is invalid. +
+ +
+ Signature: + +error ERC20InvalidCallerAddress(address _caller); + +
+
+ +
+ /// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions Revert when a provided receiver is invalid(e.g,zero address) . +
+ +
+ Signature: + +error ERC20InvalidReciever(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSender(address _sender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20BridgeableFacet} from "../interfaces/IERC20BridgeableFacet.sol"; +import {IDiamondStorage} from "../interfaces/IDiamondStorage.sol"; + +contract ERC20BridgeableConsumerFacet { + address immutable DIAMOND_ADDRESS; + + constructor(address diamondAddress) { + DIAMOND_ADDRESS = diamondAddress; + } + + function consumeCrosschainMint(address _to, uint256 _amount) external { + IERC20BridgeableFacet(DIAMOND_ADDRESS).crosschainMint(_to, _amount); + } + + function consumeCrosschainBurn(address _from, uint256 _amount) external { + IERC20BridgeableFacet(DIAMOND_ADDRESS).crosschainBurn(_from, _amount); + } +}`} + + +## Best Practices + + +- Ensure only addresses with the `trusted-bridge` role can call `crosschainBurn` and `crosschainMint`. +- Validate `_to` and `_from` addresses to prevent sending tokens to zero or invalid addresses. +- Handle `ERC20InsufficientBalance` and `ERC20InvalidReciever` errors gracefully. + + +## Integration Notes + + +This module interacts with the diamond's storage through predefined slots for AccessControl and ERC20 state. The `getAccessControlStorage` and `getERC20Storage` functions provide direct access to these structs. The `checkTokenBridge` internal function enforces access control by verifying the caller's role in the AccessControl storage. No specific storage slot ordering is mandated for this module itself, but it relies on the underlying diamond storage structure. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20Bridgeable/_category_.json b/website/docs/library/token/ERC20/ERC20Bridgeable/_category_.json new file mode 100644 index 00000000..03768f44 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Bridgeable/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-20 Bridgeable", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/ERC20Bridgeable/index" + } +} diff --git a/website/docs/library/token/ERC20/ERC20Bridgeable/index.mdx b/website/docs/library/token/ERC20/ERC20Bridgeable/index.mdx new file mode 100644 index 00000000..a85206ad --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Bridgeable/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Bridgeable" +description: "ERC-20 Bridgeable extension for ERC-20 tokens." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-20 Bridgeable extension for ERC-20 tokens. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitFacet.mdx b/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitFacet.mdx new file mode 100644 index 00000000..f6137591 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitFacet.mdx @@ -0,0 +1,339 @@ +--- +sidebar_position: 99 +title: "ERC20PermitFacet" +description: "EIP-2612 compliant ERC-20 permit functionality." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20Permit/ERC20PermitFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +EIP-2612 compliant ERC-20 permit functionality. + + + +- Implements EIP-2612 permit functionality for ERC-20 tokens. +- Enables gasless approvals by allowing users to sign allowance requests off-chain. +- Utilizes nonces and domain separators to prevent replay attacks and ensure signature validity. + + +## Overview + +The ERC20PermitFacet enables gasless approvals for ERC-20 tokens by implementing EIP-2612's permit functionality. Users can grant allowances to spenders via signed messages, which can then be submitted by any party to the diamond, bypassing the need for the user to pay gas for the approval transaction. + +--- + +## Storage + +### ERC20Storage + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + uint8 decimals; + string name; +}`} + + +--- +### ERC20PermitStorage + + +{`struct ERC20PermitStorage { + mapping(address owner => uint256) nonces; +}`} + + +### State Variables + + + +## Functions + +### nonces + +Returns the current nonce for an owner. This value changes each time a permit is used. + + +{`function nonces(address _owner) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### DOMAIN_SEPARATOR + +Returns the domain separator used in the encoding of the signature for permit. This value is unique to a contract and chain ID combination to prevent replay attacks. + + +{`function DOMAIN_SEPARATOR() external view returns (bytes32);`} + + +**Returns:** + + + +--- +### permit + +Sets the allowance for a spender via a signature. This function implements EIP-2612 permit functionality. + + +{`function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s +) external;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when an approval is made for a spender by an owner. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when a permit signature is invalid or expired. +
+ +
+ Signature: + +error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s +); + +
+
+ +
+ Thrown when the spender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSpender(address _spender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { DiamondLoupeFacet } from "@openzeppelin/contracts/facets/DiamondLoupeFacet.sol"; +import { FacetNames } from "@openzeppelin/contracts/facets/FacetNames.sol"; + +// Assume Diamond interface and DiamondProxy are deployed elsewhere +interface IDiamond { + function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external; + function facetAddress(bytes4 _functionSelector) external view returns (address _facetAddress); + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory _selectors); + function facets() external view returns (Facet[] memory _facets); +} + +interface IERC20PermitFacet { + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + function nonces(address owner) external view returns (uint256); + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +contract ERC20PermitDeployer { + // ... deployment logic ... + + function grantPermit(address _diamondProxyAddress, address _tokenAddress, address _spender, uint256 _amount, uint256 _deadline) public { + // Fetch nonce and domain separator from the diamond + IERC20PermitFacet permitFacet = IERC20PermitFacet(_diamondProxyAddress); + bytes32 domainSeparator = permitFacet.DOMAIN_SEPARATOR(); + uint256 nonce = permitFacet.nonces(msg.sender); + + // Construct the permit message hash + bytes32 digest = keccak256( + abi.encode( IERC20Permit.permitHash(), msg.sender, _spender, _amount, nonce, _deadline) + ); + + // Sign the digest (this would typically be done off-chain) + // For demonstration, assume \`v\`, \`r\`, \`s\` are obtained from an external signature + uint8 v; + bytes32 r; + bytes32 s; + + // Submit the permit to the diamond + // Note: This assumes the ERC20 token contract is accessible and has an \`approve\` function + // and that the diamond proxy is configured to route permit calls to the ERC20PermitFacet. + permitFacet.permit(_tokenAddress, _spender, _amount, _deadline, v, r, s); + } +}`} + + +## Best Practices + + +- Integrate the `ERC20PermitFacet` into your diamond, ensuring its function selectors are correctly routed. +- Store the `DOMAIN_SEPARATOR` and `nonces` mapping within the diamond's storage or a dedicated facet for consistent access. +- Off-chain signing of permit messages is crucial for enabling gasless approvals. The signed data is then submitted on-chain by any party. + + +## Security Considerations + + +Ensure the `DOMAIN_SEPARATOR` is correctly computed and unique per chain ID and contract instance. The `nonces` mapping must be managed carefully to prevent permit reuse. Validate the signature parameters (`v`, `r`, `s`) and the `owner` address before setting allowances. Access to the `permit` function should be controlled if necessary, although typically it's intended to be permissionless once the signature is valid. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitMod.mdx b/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitMod.mdx new file mode 100644 index 00000000..68d39d9e --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Permit/ERC20PermitMod.mdx @@ -0,0 +1,281 @@ +--- +sidebar_position: 99 +title: "ERC20PermitMod" +description: "ERC-2612 Permit and domain separator logic" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/ERC20Permit/ERC20PermitMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +ERC-2612 Permit and domain separator logic + + + +- Implements ERC-2612 Permit functionality for gasless token approvals. +- Manages and provides the domain separator for signature validation. +- Includes explicit error handling for invalid signatures and disallowed spenders. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides the core logic for ERC-2612 permit functionality, enabling gasless approvals via signed messages. It manages the domain separator and permit validation, ensuring secure and efficient token approvals within a diamond. + +--- + +## Storage + +### ERC20PermitStorage + +storage-location: erc8042:compose.erc20.permit + + +{`struct ERC20PermitStorage { + mapping(address owner => uint256) nonces; +}`} + + +--- +### ERC20Storage + +storage-location: erc8042:compose.erc20 + + +{`struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; + uint8 decimals; + string name; +}`} + + +### State Variables + + + +## Functions + +### DOMAIN_SEPARATOR + +Returns the domain separator used in the encoding of the signature for {permit}. This value is unique to a contract and chain ID combination to prevent replay attacks. + + +{`function DOMAIN_SEPARATOR() view returns (bytes32);`} + + +**Returns:** + + + +--- +### getERC20Storage + + +{`function getERC20Storage() pure returns (ERC20Storage storage s);`} + + +--- +### getPermitStorage + + +{`function getPermitStorage() pure returns (ERC20PermitStorage storage s);`} + + +--- +### permit + +Validates a permit signature and sets allowance. Emits Approval event; must be emitted by the calling facet/contract. + + +{`function permit(address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when an approval is made for a spender by an owner. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the spender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC20InvalidSpender(address _spender); + +
+
+ +
+ Thrown when a permit signature is invalid or expired. +
+ +
+ Signature: + +error ERC2612InvalidSignature( +address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s +); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {ERC20PermitMod} from "@compose-protocol/diamond-contracts/modules/erc20/ERC20PermitMod.sol"; + +contract MyTokenFacet { + using ERC20PermitMod for ERC20PermitMod.PermitStorage; + + ERC20PermitMod.PermitStorage public permitStorage; + + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + returns (bool) + { + // Ensure the permit storage is initialized or managed appropriately + // For example, if it's part of a larger diamond storage struct: + // ERC20PermitMod.PermitStorage storage ps = ERC20PermitMod.getPermitStorage(diamondStorage); + // permitStorage.permit(owner, spender, value, deadline, v, r, s); + + // For a standalone facet, you'd manage permitStorage directly: + return permitStorage.permit(owner, spender, value, deadline, v, r, s); + } + + // Other ERC20 functions and facet logic... +}`} + + +## Best Practices + + +- Ensure the `ERC20PermitMod.PermitStorage` is correctly initialized and accessible within your facet or diamond storage. +- Implement access control for the `permit` function if necessary, though ERC-2612 is designed to be owner-driven. +- Verify the `deadline` parameter to prevent stale permit approvals. + + +## Integration Notes + + +This module requires access to its `PermitStorage` struct, which should be managed either within the diamond's main storage or a dedicated slot. The `permit` function within this module validates the signature and updates the allowance; the calling facet is responsible for emitting the `Approval` event if required by the ERC-20 implementation standard. The domain separator is crucial for preventing cross-chain or cross-contract replay attacks. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC20/ERC20Permit/_category_.json b/website/docs/library/token/ERC20/ERC20Permit/_category_.json new file mode 100644 index 00000000..7932c4df --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Permit/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-20 Permit", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/ERC20Permit/index" + } +} diff --git a/website/docs/library/token/ERC20/ERC20Permit/index.mdx b/website/docs/library/token/ERC20/ERC20Permit/index.mdx new file mode 100644 index 00000000..1ee93f31 --- /dev/null +++ b/website/docs/library/token/ERC20/ERC20Permit/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Permit" +description: "ERC-20 Permit extension for ERC-20 tokens." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-20 Permit extension for ERC-20 tokens. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/_category_.json b/website/docs/library/token/ERC20/_category_.json new file mode 100644 index 00000000..0e078cb1 --- /dev/null +++ b/website/docs/library/token/ERC20/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-20", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/index" + } +} diff --git a/website/docs/library/token/ERC20/index.mdx b/website/docs/library/token/ERC20/index.mdx new file mode 100644 index 00000000..0bb39d2d --- /dev/null +++ b/website/docs/library/token/ERC20/index.mdx @@ -0,0 +1,37 @@ +--- +title: "ERC-20" +description: "ERC-20 fungible token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-20 fungible token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC6909/ERC6909/ERC6909Facet.mdx b/website/docs/library/token/ERC6909/ERC6909/ERC6909Facet.mdx new file mode 100644 index 00000000..36e2b49f --- /dev/null +++ b/website/docs/library/token/ERC6909/ERC6909/ERC6909Facet.mdx @@ -0,0 +1,513 @@ +--- +sidebar_position: 99 +title: "ERC6909Facet" +description: "Manage ERC-6909 compliant token balances and operator roles." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC6909/ERC6909/ERC6909Facet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage ERC-6909 compliant token balances and operator roles. + + + +- Implements core ERC-6909 transfer and allowance logic. +- Supports operator roles for delegated spending. +- Provides `getStorage` for direct state inspection (use with caution). +- Emits standard `Transfer` and `Approval` events. + + +## Overview + +This facet implements the ERC-6909 standard, providing functionality to manage token balances, allowances, and operator relationships within a Compose diamond. It enables standard token transfers and operator approvals, enhancing composability for tokenized assets. + +--- + +## Storage + +### ERC6909Storage + + +{`struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +}`} + + +### State Variables + + + +## Functions + +### balanceOf + +Owner balance of an id. + + +{`function balanceOf(address _owner, uint256 _id) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### allowance + +Spender allowance of an id. + + +{`function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### isOperator + +Checks if a spender is approved by an owner as an operator. + + +{`function isOperator(address _owner, address _spender) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### transfer + +Transfers an amount of an id from the caller to a receiver. + + +{`function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### transferFrom + +Transfers an amount of an id from a sender to a receiver. + + +{`function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### approve + +Approves an amount of an id to a spender. + + +{`function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### setOperator + +Sets or removes a spender as an operator for the caller. + + +{`function setOperator(address _spender, bool _approved) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +## Events + + + + +
+ Signature: + +{`event Transfer( + address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount +);`} + +
+ +
+ + +
+ Signature: + +{`event OperatorSet(address indexed _owner, address indexed _spender, bool _approved);`} + +
+ +
+ + +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + +
+
+ + +
+ Signature: + +error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + +
+
+ + +
+ Signature: + +error ERC6909InvalidReceiver(address _receiver); + +
+
+ + +
+ Signature: + +error ERC6909InvalidSender(address _sender); + +
+
+ + +
+ Signature: + +error ERC6909InvalidSpender(address _spender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC6909Facet} from "@compose/facets/erc6909/IERC6909Facet.sol"; +import {IERC165} from "@compose/core/IERC165.sol"; + +contract MyDiamond is IERC165 { + // ... other facet interfaces and implementations ... + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + // ... other interface checks ... + if (interfaceId == type(IERC6909Facet).interfaceId) { + return true; + } + return false; + } + + // Example of calling transfer from another facet or contract + function performTransfer(address _to, uint256 _amount, uint256 _id) external { + // Assuming IERC6909Facet is registered and callable + (bool success, ) = address(this).call(abi.encodeWithSelector(IERC6909Facet.transfer.selector, + _to, _amount, _id)); + require(success, "Transfer failed"); + } + + // Example of approving an operator + function grantOperatorRole(address _operator, uint256 _id) external { + // Assuming IERC6909Facet is registered and callable + (bool success, ) = address(this).call(abi.encodeWithSelector(IERC6909Facet.setOperator.selector, + _operator, _id, true)); + require(success, "Set operator failed"); + } +}`} + + +## Best Practices + + +- Initialize the facet with appropriate access controls during diamond deployment. +- Ensure token IDs and amounts are validated before calling transfer or approve functions. +- Store the facet's address securely and manage upgrades carefully to maintain state integrity. + + +## Security Considerations + + +Input validation is crucial; ensure `_to`, `_id`, and `_amount` parameters are valid to prevent unexpected behavior. The `transferFrom` function requires careful management of allowances to prevent unauthorized spending. Access to `setOperator` should be restricted to prevent malicious operators from being set. Direct access to storage via `getStorage` bypasses function logic and should only be used in controlled environments. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC6909/ERC6909/ERC6909Mod.mdx b/website/docs/library/token/ERC6909/ERC6909/ERC6909Mod.mdx new file mode 100644 index 00000000..f0640918 --- /dev/null +++ b/website/docs/library/token/ERC6909/ERC6909/ERC6909Mod.mdx @@ -0,0 +1,525 @@ +--- +sidebar_position: 99 +title: "ERC6909Mod" +description: "Implements ERC-6909 minimal multi-token logic." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC6909/ERC6909/ERC6909Mod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Implements ERC-6909 minimal multi-token logic. + + + +- Supports multiple token IDs within a single contract context. +- Implements standard ERC-6909 functions for token management. +- Allows for flexible operator approvals to facilitate trading and management. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides the core logic and storage for implementing the ERC-6909 standard. It enables the management of multiple token types within a single contract, supporting minting, burning, transfers, and operator approvals. By adhering to the ERC-6909 standard, diamonds can offer flexible and interoperable multi-token functionality. + +--- + +## Storage + +### ERC6909Storage + +storage-location: erc8042:compose.erc6909 + + +{`struct ERC6909Storage { + mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf; + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance; + mapping(address owner => mapping(address spender => bool)) isOperator; +}`} + + +### State Variables + + + +## Functions + +### approve + +Approves an amount of an id to a spender. + + +{`function approve(address _owner, address _spender, uint256 _id, uint256 _amount) ;`} + + +**Parameters:** + + + +--- +### burn + +Burns `_amount` of token id `_id` from `_from`. + + +{`function burn(address _from, uint256 _id, uint256 _amount) ;`} + + +**Parameters:** + + + +--- +### getStorage + +Returns a pointer to the ERC-6909 storage struct. Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + + +{`function getStorage() pure returns (ERC6909Storage storage s);`} + + +**Returns:** + + + +--- +### mint + +Mints `_amount` of token id `_id` to `_to`. + + +{`function mint(address _to, uint256 _id, uint256 _amount) ;`} + + +**Parameters:** + + + +--- +### setOperator + +Sets or removes a spender as an operator for the caller. + + +{`function setOperator(address _owner, address _spender, bool _approved) ;`} + + +**Parameters:** + + + +--- +### transfer + +Transfers `_amount` of token id `_id` from `_from` to `_to`. Allowance is not deducted if it is `type(uint256).max` Allowance is not deducted if `_by` is an operator for `_from`. + + +{`function transfer(address _by, address _from, address _to, uint256 _id, uint256 _amount) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when an approval occurs. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when an operator is set. +
+ +
+ Signature: + +{`event OperatorSet(address indexed _owner, address indexed _spender, bool _approved);`} + +
+ +
+ Parameters: + +
+
+ +
+ Emitted when a transfer occurs. +
+ +
+ Signature: + +{`event Transfer( +address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount +);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the spender has insufficient allowance. +
+ +
+ Signature: + +error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); + +
+
+ +
+ Thrown when the sender has insufficient balance. +
+ +
+ Signature: + +error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); + +
+
+ +
+ Thrown when the approver address is invalid. +
+ +
+ Signature: + +error ERC6909InvalidApprover(address _approver); + +
+
+ +
+ Thrown when the receiver address is invalid. +
+ +
+ Signature: + +error ERC6909InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid. +
+ +
+ Signature: + +error ERC6909InvalidSender(address _sender); + +
+
+ +
+ Thrown when the spender address is invalid. +
+ +
+ Signature: + +error ERC6909InvalidSpender(address _spender); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC6909Mod} from "@compose/contracts/modules/erc6909/IERC6909Mod.sol"; +import {ERC6909ModStorage} from "@compose/contracts/modules/erc6909/ERC6909ModStorage.sol"; + +contract MyERC6909Facet { + + function approve(address _spender, uint256 _amount, uint256 _id) external { + IERC6909Mod(msg.sender).approve(_spender, _amount, _id); + } + + function transfer(address _from, address _to, uint256 _amount, uint256 _id) external { + IERC6909Mod(msg.sender).transfer(_from, _to, _amount, _id); + } + + function mint(address _to, uint256 _amount, uint256 _id) external { + IERC6909Mod(msg.sender).mint(_to, _amount, _id); + } + + function burn(address _from, uint256 _amount, uint256 _id) external { + IERC6909Mod(msg.sender).burn(_from, _amount, _id); + } + + function setOperator(address _operator, bool _approved) external { + IERC6909Mod(msg.sender).setOperator(_operator, _approved); + } +}`} + + +## Best Practices + + +- Ensure appropriate access control is implemented in facets calling `mint` and `burn` functions. +- Handle custom errors like `ERC6909InsufficientBalance` and `ERC6909InsufficientAllowance` gracefully in calling facets. +- Be mindful of operator approvals; they grant significant spending power for specific token IDs. + + +## Integration Notes + + +The ERC6909Mod uses a dedicated storage slot defined by `STORAGE_POSITION`. Facets interacting with this module should be aware of the `ERC6909ModStorage` struct layout and ensure no storage collisions occur. The `getStorage` function provides a direct pointer to this storage for internal use by facets. Changes to allowances or balances are managed within this module's storage and are visible to all facets interacting with the diamond. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC6909/ERC6909/_category_.json b/website/docs/library/token/ERC6909/ERC6909/_category_.json new file mode 100644 index 00000000..d4d084dc --- /dev/null +++ b/website/docs/library/token/ERC6909/ERC6909/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-6909", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC6909/ERC6909/index" + } +} diff --git a/website/docs/library/token/ERC6909/ERC6909/index.mdx b/website/docs/library/token/ERC6909/ERC6909/index.mdx new file mode 100644 index 00000000..c902a388 --- /dev/null +++ b/website/docs/library/token/ERC6909/ERC6909/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-6909" +description: "ERC-6909 minimal multi-token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-6909 minimal multi-token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC6909/_category_.json b/website/docs/library/token/ERC6909/_category_.json new file mode 100644 index 00000000..42f1101f --- /dev/null +++ b/website/docs/library/token/ERC6909/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-6909", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC6909/index" + } +} diff --git a/website/docs/library/token/ERC6909/index.mdx b/website/docs/library/token/ERC6909/index.mdx new file mode 100644 index 00000000..dab5e87f --- /dev/null +++ b/website/docs/library/token/ERC6909/index.mdx @@ -0,0 +1,23 @@ +--- +title: "ERC-6909" +description: "ERC-6909 minimal multi-token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-6909 minimal multi-token implementations. + + + + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC721/ERC721/ERC721BurnFacet.mdx b/website/docs/library/token/ERC721/ERC721/ERC721BurnFacet.mdx new file mode 100644 index 00000000..bb0491a5 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721/ERC721BurnFacet.mdx @@ -0,0 +1,200 @@ +--- +sidebar_position: 99 +title: "ERC721BurnFacet" +description: "Burn ERC721 tokens within a Compose diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721/ERC721BurnFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Burn ERC721 tokens within a Compose diamond. + + + +- Burns ERC721 tokens, effectively destroying them. +- Emits standard `Transfer` events for burned tokens (from owner to address(0)). +- Utilizes inline assembly to access the correct storage slot for ERC721 state. + + +## Overview + +The ERC721BurnFacet provides the functionality to destroy ERC721 tokens. It integrates with the diamond proxy pattern to offer a composable way to manage token lifecycle, specifically the burning of owned tokens. This facet ensures that burned tokens are correctly removed from tracking and associated events are emitted. + +--- + +## Storage + +### ERC721Storage + + +{`struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns (destroys) a token, removing it from enumeration tracking. + + +{`function burn(uint256 _tokenId) external;`} + + +**Parameters:** + + + +## Events + + + + +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+ + +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IDiamondCut} from "@compose/diamond/contracts/interfaces/IDiamondCut.sol"; +import {IERC721BurnFacet} from "./interfaces/IERC721BurnFacet.sol"; + +contract Deployer { + address immutable diamondAddress; + + constructor(address _diamondAddress) { + diamondAddress = _diamondAddress; + } + + function addBurnFacet(address _burnFacetImplementation) external { + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = IERC721BurnFacet.getStorage.selector; + selectors[1] = IERC721BurnFacet.burn.selector; + + IDiamondCut(diamondAddress).diamondCut([ + IDiamondCut.FacetCut({ + facetAddress: _burnFacetImplementation, + action: IDiamondCut.FacetCutAction.ADD, + isUnion: false, + selectors: selectors + }) + ], address(0), ""); + } + + function burnToken(uint256 _tokenId) external { + IERC721BurnFacet(diamondAddress).burn(_tokenId); + } +}`} + + +## Best Practices + + +- Ensure the `ERC721BurnFacet` is added to the diamond with the correct selectors. +- Call `burn` only for tokens owned by the caller or for which the caller has sufficient approval. +- Understand the storage layout by calling `getStorage` if direct interaction with underlying ERC721 state is required. + + +## Security Considerations + + +The `burn` function requires the caller to be the owner of the token or have explicit approval. Ensure that the diamond's access control mechanisms correctly enforce these ownership and approval checks before allowing the `burn` function to be executed. Reentrancy is not a concern as `burn` does not make external calls before state changes. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721/ERC721Facet.mdx b/website/docs/library/token/ERC721/ERC721/ERC721Facet.mdx new file mode 100644 index 00000000..0f5bb7c3 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721/ERC721Facet.mdx @@ -0,0 +1,615 @@ +--- +sidebar_position: 99 +title: "ERC721Facet" +description: "Manage ERC-721 tokens and metadata within a diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721/ERC721Facet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage ERC-721 tokens and metadata within a diamond. + + + +- Implements the ERC-721 standard for non-fungible tokens. +- Supports token transfers, ownership tracking, and approvals. +- Provides `tokenURI` for metadata retrieval. +- Includes internal transfer logic for robust state management. + + +## Overview + +The ERC721Facet provides a standard implementation for ERC-721 token functionality. It enables core operations such as token transfers, ownership queries, approvals, and metadata retrieval. This facet can be integrated into a diamond to offer a composable and upgradeable NFT collection. + +--- + +## Storage + +### ERC721Storage + + +{`struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + string name; + string symbol; + string baseURI; +}`} + + +### State Variables + + + +## Functions + +### name + +Returns the token collection name. + + +{`function name() external view returns (string memory);`} + + +**Returns:** + + + +--- +### symbol + +Returns the token collection symbol. + + +{`function symbol() external view returns (string memory);`} + + +**Returns:** + + + +--- +### tokenURI + +Provide the metadata URI for a given token ID. + + +{`function tokenURI(uint256 _tokenId) external view returns (string memory);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### balanceOf + +Returns the number of tokens owned by a given address. + + +{`function balanceOf(address _owner) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### ownerOf + +Returns the owner of a given token ID. + + +{`function ownerOf(uint256 _tokenId) public view returns (address);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### getApproved + +Returns the approved address for a given token ID. + + +{`function getApproved(uint256 _tokenId) external view returns (address);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### isApprovedForAll + +Returns true if an operator is approved to manage all of an owner's assets. + + +{`function isApprovedForAll(address _owner, address _operator) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### approve + +Approves another address to transfer the given token ID. + + +{`function approve(address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### setApprovalForAll + +Approves or revokes permission for an operator to manage all caller's assets. + + +{`function setApprovalForAll(address _operator, bool _approved) external;`} + + +**Parameters:** + + + +--- +### transferFrom + +Transfers a token from one address to another. + + +{`function transferFrom(address _from, address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### safeTransferFrom + +Safely transfers a token, checking if the receiver can handle ERC-721 tokens. + + +{`function safeTransferFrom(address _from, address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### safeTransferFrom + +Safely transfers a token with additional data. + + +{`function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external;`} + + +**Parameters:** + + + +## Events + + + + +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error ERC721InvalidOwner(address _owner); + +
+
+ + +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+ + +
+ Signature: + +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + +
+
+ + +
+ Signature: + +error ERC721InvalidSender(address _sender); + +
+
+ + +
+ Signature: + +error ERC721InvalidReceiver(address _receiver); + +
+
+ + +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+ + +
+ Signature: + +error ERC721InvalidApprover(address _approver); + +
+
+ + +
+ Signature: + +error ERC721InvalidOperator(address _operator); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC721Facet} from "@compose/contracts/src/facets/ERC721/IERC721Facet.sol"; + +contract MyDiamond is IERC721Facet { + // ... other facet interfaces and implementations + + address constant ERC721_FACET_ADDRESS = address(0xabc...); // Address where ERC721Facet is deployed + + function name() external view override returns (string memory) { + return IERC721Facet(ERC721_FACET_ADDRESS).name(); + } + + function symbol() external view override returns (string memory) { + return IERC721Facet(ERC721_FACET_ADDRESS).symbol(); + } + + function balanceOf(address _owner) external view override returns (uint256) { + return IERC721Facet(ERC721_FACET_ADDRESS).balanceOf(_owner); + } + + function ownerOf(uint256 _tokenId) external view override returns (address) { + return IERC721Facet(ERC721_FACET_ADDRESS).ownerOf(_tokenId); + } + + function transferFrom(address _from, address _to, uint256 _tokenId) external override { + IERC721Facet(ERC721_FACET_ADDRESS).transferFrom(_from, _to, _tokenId); + } + + // ... other functions +}`} + + +## Best Practices + + +- Initialize the ERC721Facet with a unique storage slot using `STORAGE_POSITION`. +- Grant necessary permissions for `approve` and `setApprovalForAll` operations. +- Ensure the receiver contract implements `onERC721Received` for `safeTransferFrom` if applicable. + + +## Security Considerations + + +The `internalTransferFrom` function includes checks for ownership and approvals. `safeTransferFrom` adds a layer of security by verifying receiver contract compatibility. Ensure that access control for `approve` and `setApprovalForAll` functions is correctly managed by the diamond's access control mechanism. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721/ERC721Mod.mdx b/website/docs/library/token/ERC721/ERC721/ERC721Mod.mdx new file mode 100644 index 00000000..a4241422 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721/ERC721Mod.mdx @@ -0,0 +1,362 @@ +--- +sidebar_position: 99 +title: "ERC721Mod" +description: "Manage ERC-721 tokens within a Compose diamond." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721/ERC721Mod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage ERC-721 tokens within a Compose diamond. + + + +- Supports core ERC-721 operations: mint, burn, and transfer. +- Utilizes diamond storage for persistent and shared state management. +- Includes specific error types for common ERC-721 failures. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The ERC721Mod provides essential internal logic for minting, burning, and transferring ERC-721 compliant tokens directly within a Compose diamond. It leverages the diamond storage pattern to ensure state is managed consistently and accessibly by any compliant facet, promoting composability and reducing boilerplate code for ERC-721 functionality. + +--- + +## Storage + +### ERC721Storage + + +{`struct ERC721Storage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + string name; + string symbol; + string baseURI; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns (destroys) a specific ERC-721 token. Reverts if the token does not exist. Clears ownership and approval. + + +{`function burn(uint256 _tokenId) ;`} + + +**Parameters:** + + + +--- +### getStorage + +Returns the ERC-721 storage struct from its predefined slot. Uses inline assembly to access diamond storage location. + + +{`function getStorage() pure returns (ERC721Storage storage s);`} + + +**Returns:** + + + +--- +### mint + +Mints a new ERC-721 token to the specified address. Reverts if the receiver address is zero or if the token already exists. + + +{`function mint(address _to, uint256 _tokenId) ;`} + + +**Parameters:** + + + +--- +### setMetadata + + +{`function setMetadata(string memory _name, string memory _symbol, string memory _baseURI) ;`} + + +**Parameters:** + + + +--- +### transferFrom + +Transfers ownership of a token ID from one address to another. Validates ownership, approval, and receiver address before updating state. + + +{`function transferFrom(address _from, address _to, uint256 _tokenId) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when ownership of a token changes, including minting and burning. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the sender is not the owner of the token. +
+ +
+ Signature: + +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + +
+
+ +
+ Thrown when an operator lacks sufficient approval to manage a token. +
+ +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+ +
+ Thrown when the receiver address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC721InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid (e.g., zero address). +
+ +
+ Signature: + +error ERC721InvalidSender(address _sender); + +
+
+ +
+ Thrown when attempting to interact with a non-existent token. +
+ +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC721Mod } from "@compose/modules/ERC721Mod.sol"; +import {ERC721Storage } from "@compose/modules/ERC721Mod.sol"; + +contract MyERC721Facet { + IERC721Mod public immutable erc721Mod; + + constructor(address _erc721ModAddress) { + erc721Mod = IERC721Mod(_erc721ModAddress); + } + + function mintNewToken(address _to, uint256 _tokenId) external { + // Assume _isApprovedOrOwner check is handled externally or by the module + erc721Mod.mint(_to, _tokenId); + } + + function transferMyToken(address _from, address _to, uint256 _tokenId) external { + // Assume _isApprovedOrOwner check is handled externally or by the module + erc721Mod.transferFrom(_from, _to, _tokenId); + } + + function burnMyToken(uint256 _tokenId) external { + // Assume _isApprovedOrOwner check is handled externally or by the module + erc721Mod.burn(_tokenId); + } +}`} + + +## Best Practices + + +- Implement robust access control within facets calling this module to ensure only authorized users can perform token operations. +- Always validate receiver addresses to prevent accidental token loss. +- Be aware that `setMetadata` is present but undescribed; consult the implementation if metadata management is critical. + + +## Integration Notes + + +The ERC721Mod interacts with a predefined storage slot for its `ERC721Storage` struct. Facets integrating this module can access the current state of ERC-721 tokens using the `getStorage` function. Any operations performed by this module directly modify the diamond's storage, making state changes visible to all other facets. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721/_category_.json b/website/docs/library/token/ERC721/ERC721/_category_.json new file mode 100644 index 00000000..219beb4e --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-721", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC721/ERC721/index" + } +} diff --git a/website/docs/library/token/ERC721/ERC721/index.mdx b/website/docs/library/token/ERC721/ERC721/index.mdx new file mode 100644 index 00000000..83f6f725 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721/index.mdx @@ -0,0 +1,37 @@ +--- +title: "ERC-721" +description: "ERC-721 non-fungible token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-721 non-fungible token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableBurnFacet.mdx b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableBurnFacet.mdx new file mode 100644 index 00000000..9ca60927 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableBurnFacet.mdx @@ -0,0 +1,202 @@ +--- +sidebar_position: 99 +title: "ERC721EnumerableBurnFacet" +description: "Manage ERC721 token burning and enumeration" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721Enumerable/ERC721EnumerableBurnFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manage ERC721 token burning and enumeration + + + +- Enables burning of ERC721 tokens. +- Maintains enumeration integrity by removing burned tokens from tracking. +- Provides explicit error handling for non-existent tokens and insufficient approvals. + + +## Overview + +This facet provides functionality to burn ERC721 tokens. It integrates with the ERC721 enumerable standard, ensuring that burned tokens are correctly removed from tracking and ownership records. This facet is essential for managing the lifecycle of tokens within a Compose diamond. + +--- + +## Storage + +### ERC721EnumerableStorage + + +{`struct ERC721EnumerableStorage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256[] ownerTokens) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns (destroys) a token, removing it from enumeration tracking. + + +{`function burn(uint256 _tokenId) external;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when ownership of a token changes, including burning. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when attempting to interact with a non-existent token. +
+ +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+ +
+ Thrown when the caller lacks approval to operate on the token. +
+ +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC721EnumerableBurnFacet} from "@compose-protocol/diamond-contracts/contracts/facets/ERC721/IERC721EnumerableBurnFacet.sol"; + +contract Usage { + IERC721EnumerableBurnFacet public immutable erc721EnumerableBurnFacet; + + constructor(address diamondAddress) { + // Assume diamondAddress is the address of the deployed Compose diamond + erc721EnumerableBurnFacet = IERC721EnumerableBurnFacet(diamondAddress); + } + + function burnToken(uint256 tokenId) public { + // Ensure the caller has the necessary approvals or ownership + // For simplicity, this example assumes the caller is authorized + erc721EnumerableBurnFacet.burn(tokenId); + } +}`} + + +## Best Practices + + +- Ensure the `burn` function is called with appropriate access control (e.g., token owner or approved address). +- Integrate this facet into a diamond that already implements the core ERC721 and ERC721Enumerable interfaces. +- Understand that burning a token is an irreversible action. + + +## Security Considerations + + +The `burn` function requires careful access control to prevent unauthorized token destruction. Ensure that only the token owner or an address with explicit approval can call this function. The `ERC721NonexistentToken` and `ERC721InsufficientApproval` errors provide clear feedback on invalid burn attempts. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.mdx b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.mdx new file mode 100644 index 00000000..0a00b735 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.mdx @@ -0,0 +1,686 @@ +--- +sidebar_position: 99 +title: "ERC721EnumerableFacet" +description: "Enumerable ERC-721 implementation for tracking token ownership and metadata." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Enumerable ERC-721 implementation for tracking token ownership and metadata. + + + +- Full ERC-721 compliance with enumerable extensions. +- Efficient querying of token supply, balances, and owner information. +- Supports metadata retrieval via `tokenURI`. +- Includes internal transfer logic for composability within the diamond. + + +## Overview + +This facet provides a complete ERC-721 implementation with enumerable extensions, allowing efficient querying of token supply, balances, ownership, and approvals. It surfaces standard ERC-721 functions alongside methods for retrieving token IDs by owner index. + +--- + +## Storage + +### ERC721EnumerableStorage + + +{`struct ERC721EnumerableStorage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256[] ownerTokens) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + string name; + string symbol; + string baseURI; +}`} + + +### State Variables + + + +## Functions + +### name + +Returns the name of the token collection. + + +{`function name() external view returns (string memory);`} + + +**Returns:** + + + +--- +### symbol + +Returns the symbol of the token collection. + + +{`function symbol() external view returns (string memory);`} + + +**Returns:** + + + +--- +### tokenURI + +Provide the metadata URI for a given token ID. + + +{`function tokenURI(uint256 _tokenId) external view returns (string memory);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### totalSupply + +Returns the total number of tokens in existence. + + +{`function totalSupply() external view returns (uint256);`} + + +**Returns:** + + + +--- +### balanceOf + +Returns the number of tokens owned by an address. + + +{`function balanceOf(address _owner) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### ownerOf + +Returns the owner of a given token ID. + + +{`function ownerOf(uint256 _tokenId) public view returns (address);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### tokenOfOwnerByIndex + +Returns a token ID owned by a given address at a specific index. + + +{`function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### getApproved + +Returns the approved address for a given token ID. + + +{`function getApproved(uint256 _tokenId) external view returns (address);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### isApprovedForAll + +Returns whether an operator is approved for all tokens of an owner. + + +{`function isApprovedForAll(address _owner, address _operator) external view returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### approve + +Approves another address to transfer a specific token ID. + + +{`function approve(address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### setApprovalForAll + +Approves or revokes an operator to manage all tokens of the caller. + + +{`function setApprovalForAll(address _operator, bool _approved) external;`} + + +**Parameters:** + + + +--- +### transferFrom + +Transfers a token from one address to another. + + +{`function transferFrom(address _from, address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### safeTransferFrom + +Safely transfers a token, checking for receiver contract compatibility. + + +{`function safeTransferFrom(address _from, address _to, uint256 _tokenId) external;`} + + +**Parameters:** + + + +--- +### safeTransferFrom + +Safely transfers a token with additional data. + + +{`function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external;`} + + +**Parameters:** + + + +## Events + + + + +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ + +
+ Signature: + +{`event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);`} + +
+ +
+
+ +## Errors + + + + +
+ Signature: + +error ERC721InvalidOwner(address _owner); + +
+
+ + +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+ + +
+ Signature: + +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + +
+
+ + +
+ Signature: + +error ERC721InvalidSender(address _sender); + +
+
+ + +
+ Signature: + +error ERC721InvalidReceiver(address _receiver); + +
+
+ + +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+ + +
+ Signature: + +error ERC721InvalidApprover(address _approver); + +
+
+ + +
+ Signature: + +error ERC721InvalidOperator(address _operator); + +
+
+ + +
+ Signature: + +error ERC721OutOfBoundsIndex(address _owner, uint256 _index); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC721Enumerable } from "@compose-protocol/core/src/interfaces/tokens/IERC721Enumerable.sol"; +import { DiamondUtils } from "@compose-protocol/core/src/utils/DiamondUtils.sol"; + +contract ERC721EnumerableConsumer { + IERC721Enumerable public erc721Facet; + + constructor(address diamondAddress) { + erc721Facet = IERC721Enumerable(diamondAddress); + } + + function getTokenName() external view returns (string memory) { + return erc721Facet.name(); + } + + function getTotalSupply() external view returns (uint256) { + return erc721Facet.totalSupply(); + } + + function getOwnerOfToken(uint256 tokenId) external view returns (address) { + return erc721Facet.ownerOf(tokenId); + } + + function getTokenByIndex(address owner, uint256 index) external view returns (uint256) { + return erc721Facet.tokenOfOwnerByIndex(owner, index); + } +}`} + + +## Best Practices + + +- Initialize the facet with explicit ownership or access control mechanisms if required by your application's security model. +- Leverage `tokenOfOwnerByIndex` for iterating through a specific owner's tokens, ensuring indices are within bounds to prevent errors. +- When performing transfers, prefer `safeTransferFrom` to ensure receiver contracts are compatible with ERC-721 tokens. + + +## Security Considerations + + +Ensure that access control for functions like `approve` and `setApprovalForAll` is correctly implemented at the diamond level. The `internalTransferFrom` function is intended for internal use and should not be exposed directly. Be mindful of reentrancy risks if custom logic interacts with token transfers. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableMod.mdx b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableMod.mdx new file mode 100644 index 00000000..694dc202 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721Enumerable/ERC721EnumerableMod.mdx @@ -0,0 +1,338 @@ +--- +sidebar_position: 99 +title: "ERC721EnumerableMod" +description: "Manages enumerable ERC-721 token state and operations." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC721/ERC721Enumerable/ERC721EnumerableMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages enumerable ERC-721 token state and operations. + + + +- Manages token ownership and enumeration state for ERC-721 tokens. +- Supports minting new tokens and burning existing ones, updating enumeration lists accordingly. +- Handles token transfers by updating sender and receiver enumeration data. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides core logic for enumerable ERC-721 functionality within a Compose diamond. It enables facets to mint, burn, and transfer tokens while maintaining accurate enumeration lists. By centralizing this logic, it ensures consistent state management and simplifies facet development. + +--- + +## Storage + +### ERC721EnumerableStorage + + +{`struct ERC721EnumerableStorage { + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256[] ownerTokens) ownerTokens; + mapping(uint256 tokenId => uint256 ownerTokensIndex) ownerTokensIndex; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndex; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + mapping(uint256 tokenId => address approved) approved; + string name; + string symbol; + string baseURI; +}`} + + +### State Variables + + + +## Functions + +### burn + +Burns (destroys) an existing ERC-721 token, removing it from enumeration lists. Reverts if the token does not exist or if the sender is not authorized. + + +{`function burn(uint256 _tokenId, address _sender) ;`} + + +**Parameters:** + + + +--- +### getStorage + +Returns the ERC-721 enumerable storage struct from its predefined slot. Uses inline assembly to point to the correct diamond storage position. + + +{`function getStorage() pure returns (ERC721EnumerableStorage storage s);`} + + +**Returns:** + + + +--- +### mint + +Mints a new ERC-721 token to the specified address, adding it to enumeration lists. Reverts if the receiver address is zero or if the token already exists. + + +{`function mint(address _to, uint256 _tokenId) ;`} + + +**Parameters:** + + + +--- +### transferFrom + +Transfers a token ID from one address to another, updating enumeration data. Validates ownership, approval, and receiver address before state updates. + + +{`function transferFrom(address _from, address _to, uint256 _tokenId, address _sender) ;`} + + +**Parameters:** + + + +## Events + + + +
+ Emitted when ownership of a token changes, including minting and burning. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when the sender is not the owner of the token. +
+ +
+ Signature: + +error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + +
+
+ +
+ Thrown when an operator lacks approval to manage a token. +
+ +
+ Signature: + +error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + +
+
+ +
+ Thrown when the receiver address is invalid. +
+ +
+ Signature: + +error ERC721InvalidReceiver(address _receiver); + +
+
+ +
+ Thrown when the sender address is invalid. +
+ +
+ Signature: + +error ERC721InvalidSender(address _sender); + +
+
+ +
+ Thrown when attempting to interact with a non-existent token. +
+ +
+ Signature: + +error ERC721NonexistentToken(uint256 _tokenId); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IERC721EnumerableMod} from "./interfaces/IERC721EnumerableMod.sol"; +import {ERC721EnumerableMod} from "./ERC721EnumerableMod.sol"; + +contract MyERC721Facet { + IERC721EnumerableMod private constant _ERC721_ENUMERABLE_MOD = IERC721EnumerableMod(address(this)); + + function mintToken(address _to, uint256 _tokenId) external { + _ERC721_ENUMERABLE_MOD.mint(_to, _tokenId); + } + + function burnToken(uint256 _tokenId) external { + _ERC721_ENUMERABLE_MOD.burn(_tokenId); + } + + function transferToken(address _from, address _to, uint256 _tokenId) external { + _ERC721_ENUMERABLE_MOD.transferFrom(_from, _to, _tokenId); + } +}`} + + +## Best Practices + + +- Ensure proper access control within your facet before calling module functions like `mint` and `burn`. +- Validate all input parameters (e.g., `_to` address for `mint`) to prevent unexpected reverts from the module. +- Be aware that state changes made by this module are persistent and affect all facets interacting with ERC-721 enumerable data. + + +## Integration Notes + + +The ERC721EnumerableMod interacts with a predefined storage slot within the diamond to manage its state. Facets using this module should import the relevant interface and cast the diamond's address to it. The `getStorage` function can be used by facets to access the raw storage struct directly if needed for complex operations or auditing, though direct manipulation is discouraged. State changes made by this module (e.g., token minting, burning, transfers) are visible to all facets that access the ERC-721 enumerable storage. + + +
+ +
+ + diff --git a/website/docs/library/token/ERC721/ERC721Enumerable/_category_.json b/website/docs/library/token/ERC721/ERC721Enumerable/_category_.json new file mode 100644 index 00000000..fdc633f9 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721Enumerable/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-721 Enumerable", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC721/ERC721Enumerable/index" + } +} diff --git a/website/docs/library/token/ERC721/ERC721Enumerable/index.mdx b/website/docs/library/token/ERC721/ERC721Enumerable/index.mdx new file mode 100644 index 00000000..6c35acf4 --- /dev/null +++ b/website/docs/library/token/ERC721/ERC721Enumerable/index.mdx @@ -0,0 +1,37 @@ +--- +title: "ERC-721 Enumerable" +description: "ERC-721 Enumerable extension for ERC-721 tokens." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-721 Enumerable extension for ERC-721 tokens. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC721/_category_.json b/website/docs/library/token/ERC721/_category_.json new file mode 100644 index 00000000..8ee4f288 --- /dev/null +++ b/website/docs/library/token/ERC721/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-721", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC721/index" + } +} diff --git a/website/docs/library/token/ERC721/index.mdx b/website/docs/library/token/ERC721/index.mdx new file mode 100644 index 00000000..24a9e4be --- /dev/null +++ b/website/docs/library/token/ERC721/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-721" +description: "ERC-721 non-fungible token implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-721 non-fungible token implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/Royalty/RoyaltyFacet.mdx b/website/docs/library/token/Royalty/RoyaltyFacet.mdx new file mode 100644 index 00000000..f3ab86f1 --- /dev/null +++ b/website/docs/library/token/Royalty/RoyaltyFacet.mdx @@ -0,0 +1,171 @@ +--- +sidebar_position: 99 +title: "RoyaltyFacet" +description: "Manages token royalties according to ERC-2981." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/Royalty/RoyaltyFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages token royalties according to ERC-2981. + + + +- Implements ERC-2981 `royaltyInfo` function. +- Supports token-specific royalty configurations. +- Falls back to a default royalty setting when token-specific royalties are not defined. +- Royalty calculation based on sale price in basis points. + + +## Overview + +The RoyaltyFacet implements the ERC-2981 standard, allowing tokens to specify royalty payments on secondary sales. It provides functions to retrieve royalty information for a given token ID and sale price, supporting both token-specific and default royalty configurations. + +--- + +## Storage + +### RoyaltyInfo + + +{`struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; +}`} + + +--- +### RoyaltyStorage + + +{`struct RoyaltyStorage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; +}`} + + +### State Variables + + + +## Functions + +### royaltyInfo + +Returns royalty information for a given token and sale price. Returns token-specific royalty if set, otherwise falls back to default royalty. Royalty amount is calculated as a percentage of the sale price using basis points. Implements the ERC-2981 royaltyInfo function. + + +{`function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount);`} + + +**Parameters:** + + + +**Returns:** + + + +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IRoyaltyFacet} from "@compose-protocol/diamond-contracts/contracts/facets/RoyaltyFacet.sol"; +import {IDiamondProxy} from "@compose-protocol/diamond-contracts/contracts/interfaces/IDiamondProxy.sol"; + +contract RoyaltyConsumer { + address immutable diamondProxy; + bytes4 private constant ROYALTY_INFO_SELECTOR = IRoyaltyFacet.royaltyInfo.selector; + + constructor(address _diamondProxy) { + diamondProxy = _diamondProxy; + } + + function getTokenRoyalty(uint256 _tokenId, uint256 _salePrice) external view returns (address receiver, uint256 royaltyAmount) { + (bool success, bytes memory data) = diamondProxy.call(abi.encodeWithSelector(ROYALTY_INFO_SELECTOR, _tokenId, _salePrice)); + require(success, "RoyaltyFacet: royaltyInfo call failed"); + (receiver, royaltyAmount) = abi.decode(data, (address, uint256)); + return (receiver, royaltyAmount); + } +}`} + + +## Best Practices + + +- Initialize the RoyaltyFacet with default royalty settings during diamond deployment. +- Ensure appropriate access control is configured for setting default royalties if applicable. +- When upgrading, ensure the storage layout of the RoyaltyFacet remains compatible. + + +## Security Considerations + + +The `royaltyInfo` function is read-only and does not pose reentrancy risks. Access control for setting default royalties should be strictly managed to prevent unauthorized modifications. Ensure the `STORAGE_POSITION` for royalty storage is unique and not conflicting with other facets. + + +
+ +
+ + diff --git a/website/docs/library/token/Royalty/RoyaltyMod.mdx b/website/docs/library/token/Royalty/RoyaltyMod.mdx new file mode 100644 index 00000000..38f66d01 --- /dev/null +++ b/website/docs/library/token/Royalty/RoyaltyMod.mdx @@ -0,0 +1,340 @@ +--- +sidebar_position: 99 +title: "RoyaltyMod" +description: "Manages ERC-2981 royalties for tokens and defaults." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/Royalty/RoyaltyMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Manages ERC-2981 royalties for tokens and defaults. + + + +- Implements ERC-2981 `royaltyInfo` logic, supporting token-specific and default royalties. +- Provides functions to set, update, and delete royalty configurations. +- Includes error handling for invalid royalty parameters and receivers. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +This module provides the core logic for implementing the ERC-2981 royalty standard within a Compose diamond. It handles setting and querying both token-specific and default royalty information, ensuring compliant royalty distributions. + +--- + +## Storage + +### RoyaltyInfo + +Structure containing royalty information. **Properties** + + +{`struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; +}`} + + +--- +### RoyaltyStorage + +storage-location: erc8042:compose.erc2981 + + +{`struct RoyaltyStorage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; +}`} + + +### State Variables + + + +## Functions + +### deleteDefaultRoyalty + +Removes default royalty information. After calling this function, royaltyInfo will return (address(0), 0) for tokens without specific royalty. + + +{`function deleteDefaultRoyalty() ;`} + + +--- +### getStorage + +Returns the royalty storage struct from its predefined slot. Uses inline assembly to access diamond storage location. + + +{`function getStorage() pure returns (RoyaltyStorage storage s);`} + + +**Returns:** + + + +--- +### resetTokenRoyalty + +Resets royalty information for a specific token to use the default setting. Clears token-specific royalty storage, causing fallback to default royalty. + + +{`function resetTokenRoyalty(uint256 _tokenId) ;`} + + +**Parameters:** + + + +--- +### royaltyInfo + +Queries royalty information for a given token and sale price. Returns token-specific royalty or falls back to default royalty. Royalty amount is calculated as a percentage of the sale price using basis points. Implements the ERC-2981 royaltyInfo function logic. + + +{`function royaltyInfo(uint256 _tokenId, uint256 _salePrice) view returns (address receiver, uint256 royaltyAmount);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### setDefaultRoyalty + +Sets the default royalty information that applies to all tokens. Validates receiver and fee, then updates default royalty storage. + + +{`function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) ;`} + + +**Parameters:** + + + +--- +### setTokenRoyalty + +Sets royalty information for a specific token, overriding the default. Validates receiver and fee, then updates token-specific royalty storage. + + +{`function setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) ;`} + + +**Parameters:** + + + +## Errors + + + +
+ Thrown when default royalty fee exceeds 100% (10000 basis points). +
+ +
+ Signature: + +error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + +
+
+ +
+ Thrown when default royalty receiver is the zero address. +
+ +
+ Signature: + +error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + +
+
+ +
+ Thrown when token-specific royalty fee exceeds 100% (10000 basis points). +
+ +
+ Signature: + +error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + +
+
+ +
+ Thrown when token-specific royalty receiver is the zero address. +
+ +
+ Signature: + +error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {IRoyaltyMod} from "@compose/modules/RoyaltyMod.sol"; +import {IDiamondProxy} from "@compose/diamond/IDiamondProxy.sol"; + +contract RoyaltyFacet { + address immutable DIAMOND_PROXY; + + constructor(address _diamondProxy) { + DIAMOND_PROXY = _diamondProxy; + } + + function setRoyalty(uint256 _tokenId, address _receiver, uint16 _fee) external { + IRoyaltyMod(DIAMOND_PROXY).setTokenRoyalty(_tokenId, _receiver, _fee); + } + + function getRoyaltyInfo(uint256 _tokenId, uint256 _salePrice) external view returns (address, uint256) { + return IRoyaltyMod(DIAMOND_PROXY).royaltyInfo(_tokenId, _salePrice); + } +}`} + + +## Best Practices + + +- Use `setDefaultRoyalty` sparingly, as it impacts all tokens without specific configurations. +- Validate `_receiver` and `_fee` for both `setTokenRoyalty` and `setDefaultRoyalty` to prevent invalid royalty setups. +- Be aware that calling `resetTokenRoyalty` will revert the token to using the default royalty settings. + + +## Integration Notes + + +The RoyaltyMod stores its state in a dedicated slot within the diamond's storage. Facets can access this storage via the `getStorage` function. `royaltyInfo` queries token-specific royalties first, falling back to default royalties if no token-specific configuration is found. Deleting the default royalty means `royaltyInfo` will return `(address(0), 0)` for tokens without specific royalty settings. + + +
+ +
+ + diff --git a/website/docs/library/token/Royalty/_category_.json b/website/docs/library/token/Royalty/_category_.json new file mode 100644 index 00000000..cb6b460f --- /dev/null +++ b/website/docs/library/token/Royalty/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Royalty", + "position": 5, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/Royalty/index" + } +} diff --git a/website/docs/library/token/Royalty/index.mdx b/website/docs/library/token/Royalty/index.mdx new file mode 100644 index 00000000..d570d73b --- /dev/null +++ b/website/docs/library/token/Royalty/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Royalty" +description: "ERC-2981 royalty standard implementations." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + ERC-2981 royalty standard implementations. + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/_category_.json b/website/docs/library/token/_category_.json new file mode 100644 index 00000000..3f26c2ce --- /dev/null +++ b/website/docs/library/token/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Token Standards", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/index" + } +} diff --git a/website/docs/library/token/index.mdx b/website/docs/library/token/index.mdx new file mode 100644 index 00000000..17b1ae16 --- /dev/null +++ b/website/docs/library/token/index.mdx @@ -0,0 +1,51 @@ +--- +title: "Token Standards" +description: "Token standard implementations for Compose diamonds." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Token standard implementations for Compose diamonds. + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/utils/NonReentrancyMod.mdx b/website/docs/library/utils/NonReentrancyMod.mdx new file mode 100644 index 00000000..721197d9 --- /dev/null +++ b/website/docs/library/utils/NonReentrancyMod.mdx @@ -0,0 +1,135 @@ +--- +sidebar_position: 99 +title: "NonReentrancyMod" +description: "Enforces non-reentrant execution within diamond functions." +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/libraries/NonReentrancyMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Badge from '@site/src/components/ui/Badge'; +import Callout from '@site/src/components/ui/Callout'; +import CalloutBox from '@site/src/components/ui/CalloutBox'; +import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion'; +import PropertyTable from '@site/src/components/api/PropertyTable'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; +import CodeShowcase from '@site/src/components/code/CodeShowcase'; +import RelatedDocs from '@site/src/components/docs/RelatedDocs'; +import WasThisHelpful from '@site/src/components/docs/WasThisHelpful'; +import LastUpdated from '@site/src/components/docs/LastUpdated'; +import ReadingTime from '@site/src/components/docs/ReadingTime'; +import GradientText from '@site/src/components/ui/GradientText'; +import GradientButton from '@site/src/components/ui/GradientButton'; + + +Enforces non-reentrant execution within diamond functions. + + + +- Prevents reentrant function calls to protect state integrity. +- Uses a simple uint256 storage slot for the reentrancy lock. +- Composable with any facet that manages its own storage. + + + +This module provides internal functions for use in your custom facets. Import it to access shared logic and storage. + + +## Overview + +The NonReentrancyMod provides a robust mechanism to prevent reentrant calls within your diamond's facets. By integrating this module, you ensure that a function, once entered, cannot be re-entered before it has completed its execution, safeguarding against unexpected state changes and security vulnerabilities. + +--- + +## Storage + +### State Variables + + + +## Functions + +### enter + +How to use as a library in user facets How to use as a modifier in user facets This unlocks the entry into a function + + +{`function enter() ;`} + + +--- +### exit + +This locks the entry into a function + + +{`function exit() ;`} + + +## Errors + + + +
+ Function selector - 0x43a0d067 +
+ +
+ Signature: + +error Reentrancy(); + +
+
+
+ +## Usage Example + + +{`pragma solidity ^0.8.30; + +import {LibNonReentrancy} from "@compose/contracts/src/modules/non-reentrancy/LibNonReentrancy.sol"; + +contract MyFacet { + using LibNonReentrancy for uint256; + + uint256 internal _lock; + + /** + * @notice Performs an action that must not be reentrant. + */ + function sensitiveAction() external { + _lock.enter(); // Lock the function + // ... perform sensitive operations ... + _lock.exit(); // Unlock the function + } +}`} + + +## Best Practices + + +- Always call `_lock.enter()` at the beginning of a function and `_lock.exit()` at the end. +- Ensure `_lock.exit()` is called even in cases of early returns or reverts to prevent permanent locking. +- Use the `Reentrancy` custom error for explicit error handling. + + +## Integration Notes + + +The NonReentrancyMod is designed to be integrated as a library. It relies on a single `uint256` variable within the facet's storage to act as the reentrancy lock. This variable must be initialized (though not necessarily explicitly) and managed by the facet using the `enter` and `exit` functions. The state of this lock is local to the facet and does not directly interact with or modify diamond-level storage beyond what the facet itself controls. + + +
+ +
+ + diff --git a/website/docs/library/utils/_category_.json b/website/docs/library/utils/_category_.json new file mode 100644 index 00000000..d9c087be --- /dev/null +++ b/website/docs/library/utils/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Utilities", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/utils/index" + } +} diff --git a/website/docs/library/utils/index.mdx b/website/docs/library/utils/index.mdx new file mode 100644 index 00000000..2345aaad --- /dev/null +++ b/website/docs/library/utils/index.mdx @@ -0,0 +1,23 @@ +--- +title: "Utilities" +description: "Utility libraries and helpers for diamond development." +sidebar_class_name: hidden +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Utility libraries and helpers for diamond development. + + + + } + size="medium" + /> + diff --git a/website/package.json b/website/package.json index 502302dd..d5c60934 100644 --- a/website/package.json +++ b/website/package.json @@ -11,7 +11,8 @@ "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", - "write-heading-ids": "docusaurus write-heading-ids" + "write-heading-ids": "docusaurus write-heading-ids", + "generate-docs": "cd .. && forge doc && SKIP_ENHANCEMENT=true node .github/scripts/generate-docs.js --all" }, "dependencies": { "@docusaurus/core": "3.9.2", diff --git a/website/sidebars.js b/website/sidebars.js index d684cc43..fb17ed80 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,34 +19,34 @@ const sidebars = { 'intro', { type: 'category', - label: 'Foundations', - collapsed: false, - link: { - type: 'doc', - id: 'foundations/index', - }, + label: 'Getting Started', + collapsed: true, items: [ { type: 'autogenerated', - dirName: 'foundations', + dirName: 'getting-started', }, ], }, { type: 'category', - label: 'Getting Started', - collapsed: true, + label: 'Foundations', + collapsed: false, + link: { + type: 'doc', + id: 'foundations/index', + }, items: [ { type: 'autogenerated', - dirName: 'getting-started', + dirName: 'foundations', }, ], }, { type: 'category', label: 'Design', - collapsed: false, + collapsed: true, link: { type: 'doc', id: 'design/index', @@ -58,19 +58,21 @@ const sidebars = { }, ], }, - /* { type: 'category', - label: 'Facets', + label: 'Library', collapsed: true, + link: { + type: 'doc', + id: 'library/index', + }, items: [ { type: 'autogenerated', - dirName: 'facets', + dirName: 'library', }, ], }, - */ { type: 'category', label: 'Contribution', diff --git a/website/src/components/api/PropertyTable/index.js b/website/src/components/api/PropertyTable/index.js index 496f2fc3..22fd68e0 100644 --- a/website/src/components/api/PropertyTable/index.js +++ b/website/src/components/api/PropertyTable/index.js @@ -1,6 +1,32 @@ import React from 'react'; import styles from './styles.module.css'; +/** + * Parse description string and convert markdown-style code (backticks) to JSX code elements + * @param {string|React.ReactNode} description - Description string or React element + * @returns {React.ReactNode} Description with code elements rendered + */ +function parseDescription(description) { + if (!description || typeof description !== 'string') { + return description; + } + + // Split by backticks and alternate between text and code + const parts = description.split(/(`[^`]+`)/g); + return parts.map((part, index) => { + if (part.startsWith('`') && part.endsWith('`')) { + // This is a code block + const codeContent = part.slice(1, -1); // Remove backticks + return ( + + {codeContent} + + ); + } + return {part}; + }); +} + /** * PropertyTable Component - Modern API property documentation table * Inspired by Shadcn UI design patterns @@ -51,7 +77,7 @@ export default function PropertyTable({ )} - {prop.description || prop.desc || '-'} + {prop.descriptionElement || parseDescription(prop.description || prop.desc) || '-'} {prop.default !== undefined && (
Default: {String(prop.default)} diff --git a/website/src/components/api/PropertyTable/styles.module.css b/website/src/components/api/PropertyTable/styles.module.css index d6a75d41..c50a2be5 100644 --- a/website/src/components/api/PropertyTable/styles.module.css +++ b/website/src/components/api/PropertyTable/styles.module.css @@ -20,6 +20,7 @@ .tableWrapper { position: relative; width: 100%; + max-width: 100%; border: 1px solid var(--ifm-color-emphasis-200); border-radius: 0.5rem; background: var(--ifm-background-surface-color); @@ -38,6 +39,7 @@ -webkit-overflow-scrolling: touch; scrollbar-width: thin; scrollbar-color: var(--ifm-color-emphasis-300) transparent; + max-width: 100%; } /* Custom Scrollbar */ @@ -69,9 +71,10 @@ /* Table */ .table { width: 100%; + max-width: 100%; border-collapse: separate; border-spacing: 0; - min-width: 640px; + table-layout: auto; } /* Table Header */ @@ -162,22 +165,26 @@ /* Column Styles */ .nameColumn { - width: 20%; - min-width: 180px; + width: auto; + min-width: 120px; + max-width: 25%; } .typeColumn { - width: 15%; - min-width: 140px; + width: auto; + min-width: 100px; + max-width: 20%; } .requiredColumn { - width: 12%; - min-width: 100px; + width: auto; + min-width: 80px; + max-width: 15%; } .descriptionColumn { - width: auto; + width: 1%; /* Small width forces expansion to fill remaining space in auto layout */ + min-width: 200px; } /* Name Cell */ @@ -272,6 +279,8 @@ .descriptionCell { line-height: 1.6; color: var(--ifm-color-emphasis-700); + width: 100%; /* Ensure cell expands to fill column width */ + min-width: 0; /* Allow shrinking if needed, but column width will enforce expansion */ } [data-theme='dark'] .descriptionCell { @@ -310,6 +319,27 @@ color: #93c5fd; } +/* Inline code in descriptions */ +.descriptionCell .inlineCode { + font-family: var(--ifm-font-family-monospace); + font-size: 0.8125rem; + font-weight: 500; + background: var(--ifm-color-emphasis-100); + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + color: var(--ifm-color-primary); + border: 1px solid var(--ifm-color-emphasis-200); + display: inline-block; + line-height: 1.4; + margin: 0 0.125rem; +} + +[data-theme='dark'] .descriptionCell .inlineCode { + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.2); + color: #93c5fd; +} + /* Responsive Design */ @media (max-width: 996px) { .propertyTable { diff --git a/website/src/components/code/ExpandableCode/index.js b/website/src/components/code/ExpandableCode/index.js index fa868a9b..30602b05 100644 --- a/website/src/components/code/ExpandableCode/index.js +++ b/website/src/components/code/ExpandableCode/index.js @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; +import CodeBlock from '@theme/CodeBlock'; import Icon from '../../ui/Icon'; import clsx from 'clsx'; import styles from './styles.module.css'; @@ -18,34 +19,29 @@ export default function ExpandableCode({ children }) { const [isExpanded, setIsExpanded] = useState(false); - const codeRef = React.useRef(null); - const [needsExpansion, setNeedsExpansion] = React.useState(false); - - React.useEffect(() => { - if (codeRef.current) { - const lines = codeRef.current.textContent.split('\n').length; - setNeedsExpansion(lines > maxLines); - } - }, [children, maxLines]); + const [needsExpansion, setNeedsExpansion] = useState(false); const codeContent = typeof children === 'string' ? children : children?.props?.children || ''; + const lineCount = useMemo(() => codeContent.split('\n').length, [codeContent]); + + useEffect(() => { + setNeedsExpansion(lineCount > maxLines); + }, [lineCount, maxLines]); return (
{title &&
{title}
}
-
-          {codeContent}
-        
+ {codeContent} + {needsExpansion && ( diff --git a/website/src/components/ui/Badge/styles.module.css b/website/src/components/ui/Badge/styles.module.css index eee3e93d..855d0556 100644 --- a/website/src/components/ui/Badge/styles.module.css +++ b/website/src/components/ui/Badge/styles.module.css @@ -8,6 +8,11 @@ transition: all 0.2s ease; } +/* Prevent Markdown-wrapped children from adding extra space */ +.badge p { + margin: 0; +} + /* Sizes */ .badge.small { padding: 0.25rem 0.625rem; diff --git a/website/src/components/ui/GradientButton/styles.module.css b/website/src/components/ui/GradientButton/styles.module.css index 5bead8be..9aeb3753 100644 --- a/website/src/components/ui/GradientButton/styles.module.css +++ b/website/src/components/ui/GradientButton/styles.module.css @@ -1,8 +1,12 @@ .gradientButton { position: relative; + padding-left: 0px; display: inline-flex; align-items: center; justify-content: center; + box-sizing: border-box; + line-height: 1; + vertical-align: middle; font-weight: 600; text-decoration: none; border: none; @@ -14,10 +18,21 @@ } .buttonContent { + display: inline-flex; + align-items: center; + gap: 0.35rem; + line-height: 1; position: relative; z-index: 2; } +/* Prevent Markdown-wrapped children from adding extra space */ +.gradientButton p, +.buttonContent p { + margin: 0; + color: white; +} + .buttonGlow { position: absolute; top: 50%; @@ -46,18 +61,18 @@ /* Sizes */ .gradientButton.small { - padding: 0.5rem 1rem; - font-size: 0.875rem; + padding: 0.55rem 1rem; + font-size: 0.9rem; } .gradientButton.medium { - padding: 0.65rem 1.25rem; + padding: 0.7rem 1.3rem; font-size: 1rem; } .gradientButton.large { - padding: 0.875rem 1.75rem; - font-size: 1.125rem; + padding: 0.9rem 1.75rem; + font-size: 1.05rem; } /* Variants */ @@ -66,6 +81,11 @@ color: white; } +.gradientButton:visited, +.gradientButton * { + color: inherit; +} + .gradientButton.secondary { background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%); color: white; diff --git a/website/src/theme/EditThisPage/DocsEditThisPage.js b/website/src/theme/EditThisPage/DocsEditThisPage.js new file mode 100644 index 00000000..3ed1ec80 --- /dev/null +++ b/website/src/theme/EditThisPage/DocsEditThisPage.js @@ -0,0 +1,48 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import styles from './styles.module.css'; + +/** + * DocsEditThisPage component for documentation pages + * Uses useDoc hook to access frontMatter for "View Source" link + * + * WARNING: This component should ONLY be rendered when we're certain + * we're in a docs page context. It will throw an error if used outside + * the DocProvider context. + * + * @param {string} editUrl - URL to edit the page + */ +export default function DocsEditThisPage({editUrl}) { + const {frontMatter} = useDoc(); + const viewSource = frontMatter?.gitSource; + + // Nothing to show + if (!editUrl && !viewSource) { + return null; + } + + return ( +
+ {viewSource && ( + <> + + View Source + + | + + )} + {editUrl && ( + + Edit this page + + )} +
+ ); +} + diff --git a/website/src/theme/EditThisPage/SafeDocsEditThisPage.js b/website/src/theme/EditThisPage/SafeDocsEditThisPage.js new file mode 100644 index 00000000..7da71c5d --- /dev/null +++ b/website/src/theme/EditThisPage/SafeDocsEditThisPage.js @@ -0,0 +1,35 @@ +import React from 'react'; +import DocsEditThisPage from './DocsEditThisPage'; +import SimpleEditThisPage from './SimpleEditThisPage'; + +/** + * Error boundary wrapper for DocsEditThisPage + * Catches errors if useDoc hook is called outside DocProvider context + * Falls back to SimpleEditThisPage if an error occurs + * + * @param {string} editUrl - URL to edit the page + */ +export default class SafeDocsEditThisPage extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + // If useDoc fails, fall back to simple version + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // Error caught, will render fallback + // Could log to error reporting service here if needed + } + + render() { + if (this.state.hasError) { + return ; + } + + return ; + } +} diff --git a/website/src/theme/EditThisPage/SimpleEditThisPage.js b/website/src/theme/EditThisPage/SimpleEditThisPage.js new file mode 100644 index 00000000..eb7d676c --- /dev/null +++ b/website/src/theme/EditThisPage/SimpleEditThisPage.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import styles from './styles.module.css'; + +/** + * Simple EditThisPage component for non-docs contexts (blog, etc.) + * Safe to use anywhere - doesn't require any special context + * + * @param {string} editUrl - URL to edit the page + */ +export default function SimpleEditThisPage({editUrl}) { + if (!editUrl) { + return null; + } + + return ( +
+ + Edit this page + +
+ ); +} + diff --git a/website/src/theme/EditThisPage/index.js b/website/src/theme/EditThisPage/index.js new file mode 100644 index 00000000..db62da16 --- /dev/null +++ b/website/src/theme/EditThisPage/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import {useLocation} from '@docusaurus/router'; +import SimpleEditThisPage from './SimpleEditThisPage'; +import SafeDocsEditThisPage from './SafeDocsEditThisPage'; + +/** + * Main EditThisPage component + * + * Intelligently renders the appropriate EditThisPage variant based on the current route: + * - Docs pages (/docs/*): Uses SafeDocsEditThisPage (with useDoc hook, wrapped in error boundary) + * - Other pages: Uses SimpleEditThisPage + * + * Route checking is necessary because error boundaries don't work reliably during SSR/build. + * + * @param {string} editUrl - URL to edit the page + */ +export default function EditThisPage({editUrl}) { + let isDocsPage = false; + + try { + const location = useLocation(); + const pathname = location?.pathname || ''; + + isDocsPage = pathname.startsWith('/docs/'); + } catch (error) { + isDocsPage = false; + } + + if (isDocsPage) { + return ; + } + + return ; +} diff --git a/website/src/theme/EditThisPage/styles.module.css b/website/src/theme/EditThisPage/styles.module.css new file mode 100644 index 00000000..fc7a21e0 --- /dev/null +++ b/website/src/theme/EditThisPage/styles.module.css @@ -0,0 +1,26 @@ +.wrapper { + display: inline-flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-weight: 600; + color: var(--ifm-link-color); + text-decoration: none; +} + +.link:hover { + text-decoration: underline; + color: var(--ifm-link-hover-color, var(--ifm-link-color)); +} + +.link:visited { + color: var(--ifm-link-color); +} + +