dev3-eth
Version:
ENS-on-GitHub by NameSys
356 lines (341 loc) • 15.4 kB
JavaScript
import { execSync } from 'child_process'
import { readFileSync, existsSync } from 'fs'
import readline from 'readline'
import graphics from './utils/graphics.js'
import helper from './utils/helper.js'
import constants from './utils/constants.js'
import { ethers } from 'ethers'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
require('dotenv').config()
export async function view() {
// MAIN ============================================
const space = ` `
// Confirm Github ID
async function confirm(rl) {
return new Promise(async (resolve) => {
rl.question(' ▶ Please enter your Github ID: ', async (githubID) => {
if (helper.isValidGithubID(githubID)) {
resolve(githubID)
} else {
graphics.print(' ● Bad Input', "orange")
resolve(null)
}
})
})
}
// Shows ENS Records
async function show(githubID, provider) {
if (helper.isValidGithubID(githubID)) {
let domain = `${githubID}.dev3.eth`
graphics.print(` ▢ Searching...`, 'skyblue')
graphics.print(`${space} DOMAIN: ${domain}`, "white")
let resolver
let _error = {
addr60: null,
avatar: null,
contenthash: null
}
try { // Get Resolver
resolver = await provider.getResolver(domain)
graphics.print(`${space}RESOLVER: ${resolver.address}`, "lightgreen")
} catch {
graphics.print(`${space}RESOLVER: NOT_SET`, "orange")
}
try { // Get addr60
const addr60 = await provider.resolveName(domain)
graphics.print(`${space} ADDRESS: ${addr60} [1]`, "lightgreen")
} catch (error) {
graphics.print(`${space} ADDRESS: ... [1]`, "yellow")
_error.addr60 = error
}
try { // Get avatar
const avatar = await resolver.getText('avatar')
graphics.print(`${space} AVATAR: ${avatar} [2]`, "lightgreen")
} catch (error) {
graphics.print(`${space} AVATAR: ... [2]`, "yellow")
_error.avatar = error
}
try { // Get contenthash
const contenthash = await resolver.getContentHash()
graphics.print(` CONTENTHASH: ${contenthash} [3] ${contenthash === constants.defaultContenthash ? '[default]' : ''}`, "lightgreen")
} catch (error) {
graphics.print(` CONTENTHASH: ... [3]`, "yellow")
_error.contenthash = error
}
console.log()
return _error
} else {
graphics.print(` ⨯ Bad Github ID! Quitting...`, "orange")
return null
}
}
// Views ENS Records
async function status(username, provider, rl) {
return new Promise(async (resolve) => {
if (username) {
let _error
rl.question(` ▶ Detected Github ID: ${username}. Confirm? [Y/N]: `, async (agree) => {
if (!agree || agree.toLowerCase() === 'y' || agree.toLowerCase() === 'yes') {
_error = await show(username, provider)
resolve(_error)
} else if (agree.toLowerCase() === 'n' || agree.toLowerCase() === 'no') {
const askName = await confirm(rl)
if (askName) {
_error = await show(askName, provider)
resolve(_error)
} else {
await status(username, provider, rl) // Recursive call
}
} else {
graphics.print(' ● Bad Input', "orange")
await status(username, provider, rl) // Recursive call
}
})
} else {
const askName = await confirm(rl)
let _error
if (askName) {
_error = await show(askName, provider)
resolve(_error)
} else {
await status(username, provider, rl) // Recursive call
}
}
})
}
// Debugs ENS Records
async function debug(rl, username, error) {
return new Promise(async (resolve) => {
rl.question(' ● Debug ENS Records? [Y/N]: ', async (_debug) => {
if (!_debug || _debug.toLowerCase() === 'y' || _debug.toLowerCase() === 'yes') {
graphics.print(' ▢ Verifying Records...', "skyblue")
await verifyRecords(username, _debug)
let _url = `https://${username}.github.io/records.json`
let response = await fetch(_url)
let signer
let approval
if (!response.ok) {
graphics.print(` ■ [Fetch] Failed to fetch records file \'records.json\': error ${response.status}`, "orange")
resolve(await debug(rl, username, error))
} else {
graphics.print(` ▼ [Fetch] Fetching records file: \'records.json\'`, "cyan")
const data = await response.json()
signer = data.signer
approval = data.approval
}
await verifyCloudflare(username, signer, approval, constants.approver)
rl.question(' ● Enter [index] of record to debug? (enter index [1/2/3/N]): ', async (_index) => {
if (['1', '2', '3'].includes(_index)) {
let key
if (_index === '1') key = 'addr60'
if (_index === '2') key = 'avatar'
if (_index === '3') key = 'contenthash'
console.log(' ■ [LOG]: ', error[key])
resolve([_index, signer])
} else if (_index.toLowerCase() === 'n' || _index.toLowerCase() === 'no') {
graphics.print(` ▲ OK, BYEE!`, "lightgreen")
resolve([null, signer])
rl.close()
} else {
graphics.print(' ● Bad Input', "orange")
resolve(await debug(rl, username, error))
}
})
} else if (_debug.toLowerCase() === 'n' || _debug.toLowerCase() === 'no') {
graphics.print(` ▲ OK, BYEE!`, "lightgreen")
resolve([null, null])
rl.close()
} else {
graphics.print(' ● Bad Input', "orange")
resolve(await debug(rl, username, error))
}
})
})
}
// Verifies ENS Records
async function verifyRecords(username) {
return new Promise(async (resolve) => {
let __addr60 = { ...constants.recordContent }
let __avatar = { ...constants.recordContent }
let __contenthash = { ...constants.recordContent }
const keys = [
'address/60.json',
'text/avatar.json',
'contenthash.json'
]
if (existsSync(constants.record)) {
let records = JSON.parse(readFileSync(constants.record, 'utf-8'))
__addr60.now = records.records.address.eth || null
__avatar.now = records.records.text.avatar || null
__contenthash.now = records.records.contenthash || null
}
for (var i = 0; i < keys.length; i++) {
let _url = `https://${username}.github.io/.well-known/eth/dev3/${username}/${keys[i]}`
let response = await fetch(_url)
let test
if (i === 0) test = __addr60.now || null
if (i === 1) test = __avatar.now || null
if (i === 2) test = __contenthash.now || null
if (!response.ok && test === null) {
if (test === null) {
graphics.print(` ▼ [Fetch] Skipping record file: ${keys[i]} [${i + 1}]`, "cyan")
} else {
graphics.print(` ■ [Fetch] Failed to fetch record file ${keys[i]}: error ${response.status} [${i + 1}]`, "red")
}
} else {
const data = await response.json()
if (i === 0) __addr60.value = data.value || null
if (i === 1) __avatar.value = data.value || null
if (i === 2) __contenthash.value = data.value || null
graphics.print(` ▼ [Fetch] Fetching record file: ${keys[i]} [${i + 1}]`, "cyan")
}
}
// validity flags
var flag = {
addr60: false,
avatar: false,
contenthash: false
}
// addr60
if (__addr60.value && __addr60.value !== null && helper.isAddr(__addr60.value)) { // strip '0x'
flag.addr60 = true
graphics.print(` ○ Verified record file: address/60.json [1]`, "skyblue")
} else if (!__addr60.value || __addr60.value === null) {
flag.addr60 = true
graphics.print(' ○ Empty \'addr60:\' value in \'records.json\' [1]', "skyblue")
} else {
graphics.print(' ■ Bad \'addr60:\' value in \'records.json\' [1]', "orange")
}
// avatar
if (__avatar.value && __avatar.value !== null && helper.isURL(__avatar.value)) {
flag.avatar = true
graphics.print(` ○ Verified record file: text/avatar.json [2]`, "skyblue")
} else if (!__avatar.value || __avatar.value === null) {
flag.avatar = true
graphics.print(' ○ Empty \'avatar:\' value in \'records.json\' [2]', "skyblue")
} else {
graphics.print(' ■ Bad \'avatar:\' value in \'records.json\' [2]', "orange")
}
// contenthash
if (__contenthash.value && __contenthash.value !== null && helper.isContenthash(__contenthash.value)) {
flag.contenthash = true
graphics.print(` ○ Verified record file: contenthash.json [3]`, "skyblue")
} else if (!__contenthash.value || __contenthash.value === null) {
flag.contenthash = true
graphics.print(' ○ Empty \'contenthash:\' value in \'records.json\' [3]', "skyblue")
} else {
graphics.print(' ■ Bad \'contenthash:\' value in \'records.json\' [3]', "orange")
}
/* add more ENS Records here */
if (Object.values(flag).every(value => value === true)) {
graphics.print(` ✓ Records verified!`, "lightgreen")
resolve(true)
} else {
graphics.print(` ■ Records failed verification!`, "orange")
resolve(false)
}
})
}
// Verifies Records Signature
async function verifySignature(username, index, signer) {
return new Promise(async (resolve) => {
let key
let type
if (index === 1) {
key = 'address'
type = 'address/60'
}
if (index === 2) {
key = 'avatar'
type = 'text/avatar'
}
if (index === 3) {
key = 'contenthash'
type = 'contenthash'
}
let _url = `https://${username}.github.io/.well-known/eth/dev3/${username}/${type}.json`
let response = await fetch(_url)
let value
let signature
if (!response.ok) {
graphics.print(` ■ [Signature] Failed to fetch records file \'${type}.json\': error ${response.status}`, "orange")
resolve(true)
} else {
const data = await response.json()
value = data.value
signature = data.signature
const payload = await helper.payloadRecord(
`https://${username}.github.io`,
'5',
constants.resolver,
type,
helper.genExtradata(key, value),
signer
)
const _signer = ethers.verifyMessage(payload, signature)
if (_signer === signer) {
graphics.print(` ✓ Verified Signature for: ${type}`, "lightgreen")
resolve(true)
} else {
graphics.print(` ■ Bad \'signature:\' in \'${type}.json\'`, "orange")
resolve(true)
}
}
})
}
// Verifies Cloudflare approval
async function verifyCloudflare(username, signer, approval, approver) {
const payload = await helper.payloadCloudflare(
`https://${username}.github.io`,
'5',
constants.resolver,
signer
)
const _signer = ethers.verifyMessage(payload, approval)
if (_signer === approver) {
graphics.print(` ✓ Verified Cloudflare Validation`, "lightgreen")
} else {
graphics.print(` ■ Bad Cloudflare Signature`, "orange")
}
}
// CLI
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
// WELCOME!
console.log()
graphics.print(graphics.asciiArt, 'orange')
graphics.logo()
graphics.print(graphics.viewAsciiArt, 'orange')
console.log()
// Set Alchemy
let provider
if (process.env.ALCHEMY_KEY) {
provider = new ethers.AlchemyProvider(constants.NETWORK, process.env.ALCHEMY_KEY)
} else {
provider = new ethers.AlchemyProvider(constants.NETWORK, constants.ALCHEMY_KEY_DEFAULT)
}
// MAIN ============================================
// Check status
const remoteUrl = execSync('git config --get remote.origin.url').toString().trim()
const username = remoteUrl.match(/github\.com[:/](\w+[-_]?\w+)/)[1]
let error = await status(username, provider, rl)
async function verifyAndDebug() {
// Verify record files & Cloudflare approval
let [_debug, signer] = await debug(rl, username, error);
// Verify Signatures
if (_debug && _debug !== null) {
const trigger = await verifySignature(username, Number(_debug), signer);
if (!trigger) {
graphics.print(` ▲ HOPE IT HELPED!`, "lightgreen")
rl.close()
} else {
graphics.print(' ○ Continuing de-bugger...', "skyblue")
await verifyAndDebug()
}
}
}
await verifyAndDebug()
}