UNPKG

dev3-eth

Version:
725 lines (700 loc) 23.2 kB
import { writeFileSync, readFileSync, existsSync, statSync } from "fs"; import fs from "fs/promises"; import path from "path"; import axios from "axios"; import constants from "./constants.js"; import graphics from "./graphics.js"; import * as ensContent from "./contenthash.js"; import { execSync } from "child_process"; import { ethers, SigningKey, Wallet } from "ethers"; import { createRequire } from "module"; import { formatsByName } from "@ensdomains/address-encoder"; const require = createRequire(import.meta.url); require("dotenv").config(); const SIGNER = process.env.SIGNER; // History async function history() { return new Promise(async (resolve) => { try { const response = await fetch(constants.history); if (!response.ok) { resolve(false); } const data = await response.json(); resolve(data.total); } catch (error) { resolve(null); } }); } // Creates a deep file async function createDeepFile(filePath) { try { const directory = path.dirname(filePath); await fs.mkdir(directory, { recursive: true }); await fs.writeFile( filePath, JSON.stringify(constants.recordContent, null, 2) ); graphics.print(` ○ Made record file: ${filePath}`, "skyblue"); return true; } catch (error) { console.log(error); graphics.print(` ⨯ Error creating file: ${filePath}`, "orange"); return false; } } // Checks if GitHub user exists with given GitHub ID async function githubIDExists(username) { try { const response = await axios.get( `https://api.github.com/users/${username}` ); return response.status === 200; } catch (error) { return false; } } // Checks if GitHub ID is in valid format function isValidGithubID(githubID) { // GithubID validation logic const githubIDRegex = constants.githubIDRegex; return githubIDRegex.test(githubID); } // Checks if Signing Key is in valid format function isValidSigner(signingKey) { // GithubID validation logic const keyRegex = constants.keyRegex; return keyRegex.test(signingKey); } // Checks if current directory is Git repository function isGitRepo() { try { statSync(".git"); return true; } catch (error) { return false; } } // Check for valid ethereum address function isAddr(value) { return constants.addressRegex.test(value.slice(2)); } // Check for valid URL function isURL(value) { return constants.urlRegex.test(value); } // Check for valid Avatar function isAvatar(value) { return ( constants.urlRegex.test(value) || (value.startsWith("ipfs://") && (constants.ipfsRegexCIDv0.test(value.slice(7)) || // strip 'ipfs://' constants.ipfsRegexCIDv1.test(value.slice(7)))) || // strip 'ipfs://' value.startsWith("eip155:") // CAIP-22 format ); } // Check for valid ENS Contenthash function isContenthash(value) { return ( constants.ipnsRegex.test(value.slice(7)) || // strip 'ipns://' constants.ipfsRegexCIDv0.test(value.slice(7)) || // strip 'ipfs://' constants.ipfsRegexCIDv1.test(value.slice(7)) || // strip 'ipfs://' constants.onionRegex.test(value.slice(8)) // strip 'onion://' ); } // Gets username from Git repository async function getGitRepo() { try { // Run git command to get remote URL const remoteUrl = execSync("git config --get remote.origin.url") .toString() .trim(); const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", }).trim(); const signingKey = execSync("git config --get user.signingkey") .toString() .trim(); // Extract username from GitHub remote URL const usernameMatch = remoteUrl.match(/github\.com[:/](\w+[-_]?\w+)/); const repoName = remoteUrl .split("/") .pop() .replace(/\.git$/, ""); if (usernameMatch) { return [ usernameMatch[1], branch, signingKey, repoName, existsSync("CNAME") || false, ]; } else { return [null, null, null, null, null]; } } catch (error) { return [null, null, null, null, null]; } } // Checks if the user is in a Git repo function validateGitRepo(rl) { return new Promise(async (resolve) => { const _isGitRepo = isGitRepo(); if (_isGitRepo) { const [_username, _branch, _githubKey, _repoName, _cname] = await getGitRepo(); if (_repoName.toLowerCase() === `${_username}.github.io`) { graphics.print( ` ✓ Valid git repository: ${_repoName.toLowerCase()}`, "lightgreen" ); graphics.print( ` ▲ Please ensure that Github Pages (https://${_username}.github.io/) is configured to auto-deploy upon push from default repository \'${_repoName.toLowerCase()}\'`, "yellow" ); } else { graphics.print( ` ● Detected custom git repository: ${_repoName.toLowerCase()} (default: ${_username}.github.io)`, "yellow" ); graphics.print( ` ▲ Please ensure that Github Pages (https://${_username}.github.io/) is configured to auto-deploy upon push from custom repository \'${_repoName.toLowerCase()}\'. Otherwise, please deploy it manually from \'${_repoName.toLowerCase()}\' OR switch to default repository \'${_username}.github.io\'`, "yellow" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "yellow" ); } const _synced = !(await isRemoteSynced(_branch)); if (_synced) { const _status = execSync("git status --porcelain").toString().trim(); graphics.print(" ✓ Remote tip is in sync", "lightgreen"); resolve([_isGitRepo, _username, _branch, _githubKey, _synced, _status]); } else { graphics.print( ` ■ Cannot proceed further! Remote branch is out of sync with local. please \'git push\' or \'git pull\' to sync with remote tip and then try again`, "orange" ); graphics.print( ` ⨯ Please \'git merge\' or \'git pull\' to sync with remote tip and then try again. Quitting...`, "orange" ); rl.close(); resolve([_isGitRepo, null, null, null, null, null]); } } else { graphics.print( ` ⨯ Not a git repository! Please initialise and configure as git repository first. Quitting...`, "orange" ); graphics.print(` ■ PRE-REQUISITES:`, "orange"); graphics.print( ` ▲ Please make sure that git repository is initialised and configured to push to remote branch on Github`, "orange" ); graphics.print( ` ◥ docs: https://docs.github.com/en/get-started/using-git/about-git#github-and-the-command-line`, "orange" ); graphics.print( ` ▲ Please make sure that Github Pages (https://<githubID>.github.io/) is configured to auto-deploy upon push from the remote branch`, "orange" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "orange" ); rl.close(); resolve([_isGitRepo, null, null, null, null]); } }); } // Request Github ID for login function requestGithubID(detectedUser, rl) { return new Promise((resolve) => { rl.question( ` ▶ Detected Github ID: ${detectedUser}. Confirm? [Y/N]: `, async (agree) => { if ( !agree || agree.toLowerCase() === "y" || agree.toLowerCase() === "yes" ) { resolve(true); } else if ( agree.toLowerCase() === "n" || agree.toLowerCase() === "no" ) { resolve(false); } else { graphics.print(" ● Bad Input", "orange"); resolve(await requestGithubID(detectedUser, rl)); // Recursive call } } ); }); } // Validates Github ID for login function validateGithubID(rl, suffix) { return new Promise((resolve) => { rl.question(" ▶ Please enter your Github ID: ", async (githubID) => { if (isValidGithubID(githubID)) { const _githubIDExists = await githubIDExists(githubID); if (_githubIDExists) { graphics.print(` ▲ Welcome, ${githubID}!`, "yellow"); const _ghpages = await isGithubPagesConfigured(githubID, suffix); if (_ghpages) { graphics.print( ` ✓ Github Page exists: https://${githubID}.github.io/`, "lightgreen" ); graphics.print( ` ▲ Please ensure that Github Page (https://${githubID}.github.io/) is configured to auto-deploy upon push from the remote branch`, "skyblue" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "skyblue" ); resolve(true); // Resolve the promise with true } else { if (!suffix) { graphics.print( ` ● Github Page DOES NOT exist: https://${githubID}.github.io/${suffix}`, "yellow" ); graphics.print( ` ▲ Please ensure that Github Page (https://${githubID}.github.io/) is configured to auto-deploy upon push from the remote branch`, "yellow" ); graphics.print( ` ◑ TIP: If the issue persists, try committing a minimal \'README.md\' (or \'index.html\') file to your remote repository`, "yellow" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "yellow" ); resolve(true); // Resolve the promise with true } else { graphics.print( ` ⨯ Github Page DOES NOT exist: https://${githubID}.github.io/${suffix}`, "orange" ); graphics.print( ` ▲ Please ensure that Github Page (https://${githubID}.github.io/) is configured to auto-deploy upon push from the remote branch`, "orange" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "orange" ); graphics.print(` ⨯ Quitting...`, "orange"); resolve(false); // Resolve the promise with false } } } else { graphics.print( " ⨯ Github ID Not Found! Please try again OR press CTRL + C to exit", "orange" ); resolve(await validateGithubID(rl, suffix)); // Recursive call to prompt for GithubID again } } else { graphics.print( " ⨯ Invalid Github ID! Please try again OR press CTRL + C to exit", "orange" ); resolve(await validateGithubID(rl, suffix)); // Recursive call to prompt for GithubID again } }); }); } // Skip Github ID for login function skipGithubID(detectedUser, suffix) { return new Promise(async (resolve) => { graphics.print(` ○ Continuing with Github ID: ${detectedUser}`, "skyblue"); graphics.print(` ▲ Welcome, ${detectedUser}!`, "yellow"); const _ghpages = await isGithubPagesConfigured(detectedUser, suffix); if (_ghpages) { graphics.print( ` ✓ Github Page exists: https://${detectedUser}.github.io/`, "lightgreen" ); graphics.print( ` ▲ Please ensure that Github Page (https://${detectedUser}.github.io/) is configured to auto-deploy upon push from the remote branch`, "skyblue" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "skyblue" ); resolve(true); } else { if (!suffix) { graphics.print( ` ● Github Page DOES NOT exist: https://${detectedUser}.github.io/${suffix}`, "yellow" ); graphics.print( ` ▲ Please ensure that Github Page (https://${detectedUser}.github.io/) is configured to auto-deploy upon push from the remote branch`, "yellow" ); graphics.print( ` ◑ TIP: If the issue persists, try committing a minimal \'README.md\' (or \'index.html\') file to your remote repository`, "yellow" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "yellow" ); resolve(true); // Resolve the promise with true } else { graphics.print( ` ⨯ Github Page DOES NOT exist: https://${detectedUser}.github.io/${suffix}`, "orange" ); graphics.print( ` ▲ Please ensure that Github Page (https://${detectedUser}.github.io/) is configured to auto-deploy upon push from the remote branch`, "orange" ); graphics.print( ` ◥ docs: https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site`, "orange" ); graphics.print(` ⨯ Quitting...`, "orange"); resolve(false); // Resolve the promise with false } } }); } // Sends Git Commit & Push to Remote async function sendToRemote(branch, timestamp, githubKey, files) { try { if (githubKey) { execSync( `git add ${files}; git commit -S -m "dev3: ${timestamp}"; git push -u origin ${branch}` ); } else { execSync( `git add ${files}; git commit -m "dev3: ${timestamp}"; git push -u origin ${branch}` ); } return true; } catch (error) { graphics.print(" ⨯ Failed to Commit & Push to Git. Quitting...", "orange"); return null; } } // Try Git Commit & Push async function gitCommitPush( status, validated, branch, githubKey, detectedUser, rl, files, message ) { if (validated) { return new Promise(async (resolve) => { const timestamp = Date.now(); graphics.print(` ○ Detected branch: ${branch}`, "skyblue"); if (githubKey) { graphics.print( ` ○ Detected signature fingerprint: ${githubKey}`, "skyblue" ); graphics.print( ` ○ Trying auto-update: git add ${files}; git commit -S -m "dev3: ${timestamp}"; git push -u origin ${branch}`, "skyblue" ); } else { graphics.print( ` ○ Trying auto-update: git add ${files}; git commit -m "dev3: ${timestamp}"; git push -u origin ${branch}`, "skyblue" ); } if (status !== "") { graphics.print( " ■ There are other unadded files in the repository. Please add and commit them manually before proceeding", "orange" ); graphics.print(" ⨯ Quitting...", "orange"); resolve(false); } else { rl.question(` ▶ Try git commit & push? [Y/N]: `, async (attempt) => { if ( !attempt || attempt.toLowerCase() === "y" || attempt.toLowerCase() === "yes" ) { const _pushed = await sendToRemote( branch, timestamp, githubKey, files ); resolve(_pushed); graphics.print(message, "lightgreen"); graphics.print(` ▲ BYEE!`, "lightgreen"); rl.close(); } else if ( attempt.toLowerCase() === "n" || attempt.toLowerCase() === "no" ) { graphics.print(` ▲ OK, BYEE!`, "lightgreen"); rl.close(); resolve(false); } else { graphics.print(" ● Bad Input", "orange"); resolve( await gitCommitPush( validated, branch, githubKey, detectedUser, rl, files, message ) ); // Recursive call } }); } }); } else { return new Promise(async (resolve) => { resolve(false); }); } } //Checks if remote tip is ahead of local async function isRemoteSynced(branch) { try { // Get the commit hash of the local branch const localCommit = execSync(`git rev-parse ${branch}`).toString().trim(); // Get the commit hash of the remote branch const remoteCommit = execSync(`git ls-remote origin ${branch}`) .toString() .split("\t")[0] .trim(); // Check if the remote commit is ahead of the local commit return localCommit !== remoteCommit; } catch (error) { // Handle errors, e.g., when git commands fail graphics.print(" ■ Failed to Fetch Remote Branch...", "orange"); return false; } } // Checks if GitHub Pages is configured async function isGithubPagesConfigured(username, suffix) { try { const response = await axios.get(`https://${username}.github.io/${suffix}`); return response.status === 200 && response.status !== 404; } catch (error) { return false; } } // Writes to .env & verify.json, and .gitignore async function writeConfig(signerKey) { const envContent = `SIGNER=${signerKey[0]}\nALCHEMY_KEY=`; const _verifyContent = constants.verifyContent; _verifyContent.signer = ethers.computeAddress(`0x${signerKey[0]}`); const _recordContent = constants.recordsContent; const gitignoreContent = "node_modules\n.env\npackage-lock.json"; // Write content to .env file writeFileSync(".env", envContent); // Check if .gitignore file exists if (!existsSync(".gitignore")) { // If not, create .gitignore file and add specified content writeFileSync(".gitignore", gitignoreContent); } else { // If .gitignore exists, check if it contains '.env' line const gitignoreContents = readFileSync(".gitignore", "utf-8"); if (!gitignoreContents.includes(".env")) { // If '.env' line is not present, add it to .gitignore writeFileSync(".gitignore", `${gitignoreContents}\n${envContent}`); } } // Write empty files if (!existsSync(constants.record)) writeFileSync(constants.record, JSON.stringify(_recordContent, null, 2)); // Write verify content writeFileSync(constants.verify, JSON.stringify(_verifyContent, null, 2)); // Prevents GitHub pages from ignoring hidden files if (!existsSync(".nojekyll")) writeFileSync(".nojekyll", "#"); // Prevents 404 on Github homepage if (!existsSync("README.md") && !existsSync(".nojekyll")) writeFileSync("README.md", "#"); if ( !existsSync("index.html") && !existsSync("index.htmx") && !existsSync("index.htm") && existsSync(".nojekyll") ) { writeFileSync("index.html", constants.htmlContent); } return true; } // Payload for an ENS Record async function payloadRecord( gateway, chainID, resolver, recordType, extradata, signer ) { let _toSign = `Requesting Signature To Update ENS Record\n\nGateway: ${gateway}\nResolver: eip155:${chainID}:${resolver}\nRecord Type: ${recordType}\nExtradata: ${extradata}\nSigned By: eip155:${chainID}:${signer}`; return _toSign; } // Payload of Cloudflare approval async function payloadCloudflare(gateway, chainID, resolver, signer) { let _toSign = `Requesting Signature To Approve ENS Records Signer\n\nGateway: ${gateway}\nResolver: eip155:${chainID}:${resolver}\nApproved Signer: eip155:${chainID}:${signer}`; return _toSign; } // Signs an ENS Record async function signRecord( gateway, chainID, resolver, recordType, extradata, signer ) { let _toSign = `Requesting Signature To Update ENS Record\n\nGateway: ${gateway}\nResolver: eip155:${chainID}:${resolver}\nRecord Type: ${recordType}\nExtradata: ${extradata}\nSigned By: eip155:${chainID}:${signer}`; let _key = new SigningKey( SIGNER.slice(0, 2) === "0x" ? SIGNER : "0x" + SIGNER ); let _signer = new Wallet(_key); let signature = await _signer.signMessage(_toSign); return [_toSign, signature]; } /// Encodes string values of records // returns abi.encodeWithSelector(iCallbackType.signedRecord.selector, _signer, _recordSignature, _approvedSignature, result) function encodeValue( key, value, _signer, _recordSignature, _approvedSignature ) { let encoded; let _value = ""; let type = ""; if ( [ "avatar", "email", "pubkey", "github", "url", "twitter", "x", "discord", "farcaster", "nostr", "zonehash", ].includes(key) ) { type = "string"; _value = value; } if (["btc", "ltc", "doge", "sol", "atom"].includes(key)) { type = "bytes"; _value = `0x${formatsByName[key.toUpperCase()] .decoder(value) .toString("hex")}`; } if (key === "contenthash") { type = "bytes"; _value = ensContent.encodeContenthash(value).encoded; } if (key === "address") { type = "address"; _value = value; } let _result = ethers.AbiCoder.defaultAbiCoder().encode([type], [_value]); let _ABI = [constants.signedRecord]; let _interface = new ethers.Interface(_ABI); let _encodedWithSelector = _interface.encodeFunctionData("signedRecord", [ _signer, _recordSignature, _approvedSignature, _result, ]); encoded = _encodedWithSelector; return encoded; } // Generates extradata function genExtradata(key, _recordValue) { // returns bytesToHexString(abi.encodePacked(keccak256(result))) let type = ""; let _value = ""; if ( [ "avatar", "email", "pubkey", "github", "url", "twitter", "x", "discord", "farcaster", "nostr", "zonehash", ].includes(key) ) { type = "string"; _value = _recordValue; } if (["btc", "ltc", "doge", "sol", "atom"].includes(key)) { type = "bytes"; _value = `0x${formatsByName[key.toUpperCase()] .decoder(_recordValue) .toString("hex")}`; } if (key === "contenthash") { type = "bytes"; _value = ensContent.encodeContenthash(_recordValue).encoded; } if (key === "address") { type = "address"; _value = _recordValue; } let _result = ethers.AbiCoder.defaultAbiCoder().encode([type], [_value]); const toPack = ethers.keccak256(_result); const _extradata = ethers.hexlify(ethers.solidityPacked(["bytes"], [toPack])); return _extradata; } export default { createDeepFile, githubIDExists, isValidGithubID, isValidSigner, isGitRepo, getGitRepo, writeConfig, isGithubPagesConfigured, isRemoteSynced, signRecord, requestGithubID, validateGitRepo, validateGithubID, skipGithubID, gitCommitPush, isAddr, isURL, isAvatar, isContenthash, encodeValue, genExtradata, payloadRecord, payloadCloudflare, history, };