diff --git a/.github/scripts/sync-openapi.js b/.github/scripts/sync-openapi.js new file mode 100644 index 0000000..8af2b53 --- /dev/null +++ b/.github/scripts/sync-openapi.js @@ -0,0 +1,258 @@ +#!/usr/bin/env node + +/** + * Sync OpenAPI Specification from Hedera Mirror Node + * + * This script downloads the latest OpenAPI specification from Hedera Mirror Node + * and updates the local openapi.yaml file if there are changes. + * + * Usage: + * node sync-openapi.js [--network=testnet|mainnet] [--output=path/to/openapi.yaml] + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Configuration +const NETWORKS = { + testnet: 'https://testnet.mirrornode.hedera.com/api/v1/docs/openapi.yml', + mainnet: 'https://mainnet.mirrornode.hedera.com/api/v1/docs/openapi.yml' +}; + +// Parse command line arguments +const args = process.argv.slice(2).reduce((acc, arg) => { + const [key, value] = arg.replace('--', '').split('='); + acc[key] = value || true; + return acc; +}, {}); + +const network = args.network || 'mainnet'; +const outputPath = args.output || path.join(process.cwd(), 'openapi.yaml'); +const dryRun = args['dry-run'] || false; +const verbose = args.verbose || false; + +// Validate network +if (!NETWORKS[network]) { + console.error(`â Error: Invalid network "${network}". Must be "testnet" or "mainnet".`); + process.exit(1); +} + +const sourceUrl = NETWORKS[network]; + +/** + * Log message if verbose mode is enabled + */ +function log(message) { + if (verbose) { + console.log(message); + } +} + +/** + * Download content from URL + */ +function downloadFile(url) { + return new Promise((resolve, reject) => { + log(`đĨ Downloading from ${url}...`); + + https.get(url, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: HTTP ${response.statusCode}`)); + return; + } + + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + + response.on('end', () => { + log(`â Download complete (${data.length} bytes)`); + resolve(data); + }); + }).on('error', (error) => { + reject(error); + }); + }); +} + +/** + * Extract version from OpenAPI content + */ +function extractVersion(content) { + const match = content.match(/version:\s*['"]?([0-9.]+)['"]?/); + return match ? match[1] : 'unknown'; +} + +/** + * Check if files are different + */ +function filesAreDifferent(content1, content2) { + // Normalize line endings and trim whitespace + const normalize = (str) => str.replace(/\r\n/g, '\n').trim(); + return normalize(content1) !== normalize(content2); +} + +/** + * Create backup of existing file + */ +function createBackup(filePath) { + if (fs.existsSync(filePath)) { + const backupPath = `${filePath}.backup`; + fs.copyFileSync(filePath, backupPath); + log(`â Created backup at ${backupPath}`); + return backupPath; + } + return null; +} + +/** + * Validate YAML syntax + */ +function validateYaml(content) { + try { + // Try to parse with Node.js (basic validation) + // For production, consider using a proper YAML parser like 'js-yaml' + const lines = content.split('\n'); + let indentStack = [0]; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.trim() === '' || line.trim().startsWith('#')) continue; + + const indent = line.search(/\S/); + if (indent === -1) continue; + + // Basic indentation check + if (indent > indentStack[indentStack.length - 1]) { + indentStack.push(indent); + } else { + while (indentStack.length > 0 && indent < indentStack[indentStack.length - 1]) { + indentStack.pop(); + } + } + } + + log('â YAML syntax validation passed'); + return true; + } catch (error) { + console.error(`â YAML validation failed: ${error.message}`); + return false; + } +} + +/** + * Main sync function + */ +async function syncOpenApi() { + console.log('đ Hedera Mirror Node OpenAPI Sync'); + console.log('====================================='); + console.log(`Network: ${network}`); + console.log(`Source: ${sourceUrl}`); + console.log(`Output: ${outputPath}`); + console.log(`Dry run: ${dryRun ? 'Yes' : 'No'}`); + console.log(''); + + try { + // Download new content + const newContent = await downloadFile(sourceUrl); + const newVersion = extractVersion(newContent); + + console.log(`đĻ Downloaded version: ${newVersion}`); + + // Validate YAML + if (!validateYaml(newContent)) { + throw new Error('Downloaded content failed YAML validation'); + } + + // Check if file exists and compare + let hasChanges = true; + let currentVersion = 'none'; + + if (fs.existsSync(outputPath)) { + const currentContent = fs.readFileSync(outputPath, 'utf8'); + currentVersion = extractVersion(currentContent); + hasChanges = filesAreDifferent(currentContent, newContent); + + console.log(`đ Current version: ${currentVersion}`); + + if (!hasChanges) { + console.log('â No changes detected. File is up to date.'); + return { updated: false, version: currentVersion }; + } + + console.log('đ Changes detected!'); + } else { + console.log('đ No existing file found. Creating new file.'); + } + + if (dryRun) { + console.log(''); + console.log('đ DRY RUN MODE - No files will be modified'); + console.log(`Would update from version ${currentVersion} to ${newVersion}`); + return { updated: false, version: newVersion, dryRun: true }; + } + + // Create backup + const backupPath = createBackup(outputPath); + + // Write new content + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFileSync(outputPath, newContent, 'utf8'); + console.log(`â Successfully updated ${outputPath}`); + + if (backupPath) { + console.log(`đž Backup saved to ${backupPath}`); + } + + // Summary + console.log(''); + console.log('đ Summary:'); + console.log(` Previous version: ${currentVersion}`); + console.log(` New version: ${newVersion}`); + console.log(` File size: ${newContent.length} bytes`); + + return { + updated: true, + version: newVersion, + previousVersion: currentVersion, + backupPath + }; + + } catch (error) { + console.error(''); + console.error('â Error:', error.message); + + if (verbose && error.stack) { + console.error(''); + console.error('Stack trace:'); + console.error(error.stack); + } + + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + syncOpenApi() + .then((result) => { + if (result.updated) { + process.exit(0); + } else { + process.exit(0); + } + }) + .catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +module.exports = { syncOpenApi, NETWORKS }; diff --git a/.github/workflows/sync-openapi.yml b/.github/workflows/sync-openapi.yml new file mode 100644 index 0000000..8054cc0 --- /dev/null +++ b/.github/workflows/sync-openapi.yml @@ -0,0 +1,131 @@ +name: Sync OpenAPI Specification + +on: + # Run daily at 2 AM UTC to check for updates + schedule: + - cron: '0 2 * * *' + + # Allow manual trigger + workflow_dispatch: + inputs: + network: + description: 'Network to sync from' + required: true + default: 'testnet' + type: choice + options: + - mainnet + - testnet + create_pr: + description: 'Create pull request if changes detected' + required: true + default: true + type: boolean + +jobs: + sync-openapi: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run OpenAPI sync script + id: sync + run: | + NETWORK="${{ github.event.inputs.network || 'mainnet' }}" + echo "Syncing from $NETWORK network..." + + # Run the sync script + node sync-openapi.js --network=$NETWORK --output=openapi.yaml --verbose + + # Check if there are changes + if git diff --quiet openapi.yaml; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "No changes detected" + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "Changes detected" + + # Extract version from the new file + VERSION=$(grep -m 1 "version:" openapi.yaml | sed 's/.*version: *//;s/["\x27]//g') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Detected version: $VERSION" + fi + + - name: Create Pull Request + if: steps.sync.outputs.has_changes == 'true' && (github.event.inputs.create_pr != 'false' || github.event_name == 'schedule') + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: update OpenAPI spec to v${{ steps.sync.outputs.version }}' + title: 'Update Mirror Node OpenAPI Specification to v${{ steps.sync.outputs.version }}' + body: | + ## đ Automated OpenAPI Sync + + This PR updates the OpenAPI specification from the Hedera Mirror Node API. + + **Details:** + - **Network:** ${{ github.event.inputs.network || 'mainnet' }} + - **Version:** ${{ steps.sync.outputs.version }} + - **Source:** https://${{ github.event.inputs.network || 'mainnet' }}.mirrornode.hedera.com/api/v1/docs/openapi.yml + - **Triggered by:** ${{ github.event_name }} + + **Changes:** + This automated sync detected changes in the Mirror Node OpenAPI specification. Please review the changes to ensure they align with the documentation requirements. + + **Review Checklist:** + - [ ] Verify new endpoints are documented + - [ ] Check for breaking changes + - [ ] Update related documentation if needed + - [ ] Validate YAML syntax + + --- + + đ¤ This PR was automatically created by the OpenAPI sync workflow. + + **Related Links:** + - [Mirror Node Release Notes](https://docs.hedera.com/hedera/networks/release-notes/mirror-node) + - [Mirror Node API Docs](https://docs.hedera.com/hedera/sdks-and-apis/rest-api) + branch: automated/sync-openapi-${{ steps.sync.outputs.version }} + delete-branch: true + labels: | + automated + documentation + mirror-node + openapi + + - name: Commit changes directly (if no PR) + if: steps.sync.outputs.has_changes == 'true' && github.event.inputs.create_pr == 'false' && github.event_name != 'schedule' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add openapi.yaml + git commit -m "chore: update OpenAPI spec to v${{ steps.sync.outputs.version }}" + git push + + - name: Summary + run: | + echo "## OpenAPI Sync Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Network:** ${{ github.event.inputs.network || 'mainnet' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Has Changes:** ${{ steps.sync.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.sync.outputs.has_changes }}" == "true" ]; then + echo "- **New Version:** ${{ steps.sync.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "â OpenAPI specification has been updated." >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "âšī¸ No changes detected. OpenAPI specification is up to date." >> $GITHUB_STEP_SUMMARY + fi diff --git a/hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx b/hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx index d4d29f6..34e0895 100644 --- a/hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx +++ b/hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx @@ -26,3 +26,6 @@ Hedera is fully compatible with web3 wallets like MetaMask. Just add the JSON-RP />
| Name | Value |
|---|---|
| Network Name | Hedera Testnet |
| RPC Endpoint | https://testnet.hashio.io/api |
| Chain ID | 296 |
| Currency Symbol | HBAR |
| Block Explorer URL | https://hashscan.io/testnet |