dev3-eth
Version:
ENS-on-GitHub by NameSys
709 lines (695 loc) • 22.9 kB
JavaScript
import readline from "readline";
import constants from "./utils/constants.js";
import graphics from "./utils/graphics.js";
import helper from "./utils/helper.js";
import { writeFileSync, readFileSync, existsSync } from "fs";
import { createRequire } from "module";
import { execSync } from "child_process";
const require = createRequire(import.meta.url);
require("dotenv").config();
export async function sign() {
// FUNC ============================================
// Initiates writing ENS Records
async function writeRecords(welcome) {
if (welcome) {
return new Promise(async (resolve) => {
graphics.print(
` ◑ TIP: ENS Records can be added in the next step or manually updated in \'records.json\' file`,
"skyblue"
);
rl.question(
" ▶ Continue in next step? [Y] OR, update manually? [N]: ",
async (auto) => {
if (auto.toLowerCase() === "y" || auto.toLowerCase() === "yes") {
resolve(true);
} else if (
auto.toLowerCase() === "n" ||
auto.toLowerCase() === "no"
) {
resolve(false);
} else {
graphics.print(" ● Bad Input", "orange");
resolve(await writeRecords(welcome)); // Recursive call
}
}
);
});
}
}
// Writes ENS Records: addr60
async function write_addr60(_addr60_, welcome, written) {
if (welcome && written) {
return new Promise(async (resolve) => {
rl.question(
" ▶ Please enter your ETH address (address/60) and then press ENTER: ",
async (_addr60) => {
if (_addr60) {
if (helper.isAddr(_addr60)) {
// strip '0x'
_addr60_[0].value = _addr60;
resolve([true, _addr60_]);
} else {
graphics.print(" ● Bad Input", "orange");
resolve(await write_addr60(_addr60_, welcome, written)); // Recursive call
}
} else {
_addr60_[0].value = null;
resolve([true, _addr60_]);
}
}
);
});
} else {
return new Promise(async (resolve) => {
resolve([false, _addr60_]);
});
}
}
// Writes ENS Records: avatar
async function write_avatar(_avatar_, welcome, written, written_addr60) {
if (welcome && written && written_addr60) {
return new Promise(async (resolve) => {
rl.question(
" ▶ Please enter avatar URL (text/avatar) and then press ENTER: ",
async (_avatar) => {
if (_avatar) {
if (helper.isURL(_avatar)) {
_avatar_[0].value = _avatar;
resolve([true, _avatar_]);
} else {
graphics.print(" ● Bad Input", "orange");
resolve(
await write_avatar(_avatar_, welcome, written, written_addr60)
); // Recursive call
}
} else {
_avatar_[0].value = null;
resolve([true, _avatar_]);
}
}
);
});
} else {
return new Promise(async (resolve) => {
resolve([false, _avatar_]);
});
}
}
// Writes ENS Records: contenthash
async function write_contenthash(
_contenthash_,
welcome,
written,
written_addr60,
written_avatar
) {
if (welcome && written && written_addr60 && written_avatar) {
return new Promise(async (resolve) => {
rl.question(
" ▶ Please enter contenthash value and then press ENTER: ",
async (_contenthash) => {
if (_contenthash) {
if (helper.isContenthash(_contenthash)) {
_contenthash_[0].value = _contenthash;
resolve([true, _contenthash_]);
} else {
graphics.print(" ● Bad Input! Resetting...", "orange");
resolve(
await write_contenthash(
_contenthash_,
welcome,
written,
written_addr60,
written_avatar
)
); // Recursive call
}
} else {
_contenthash_[0].value = constants.defaultContenthash;
resolve([true, _contenthash_]);
}
}
);
});
} else {
return new Promise(async (resolve) => {
resolve([false, _contenthash_]);
});
}
}
// Confirms ENS Records
async function confirmRecords(
detectedUser,
written,
written_contenthash,
addr60,
avatar,
contenthash
) {
if (welcome && !written && !written_contenthash) {
return new Promise(async (resolve) => {
rl.question(
` ▶ Please manually edit record keys in \'records.json\' file, save the file and then press ENTER: `,
async (done) => {
let _buffer = JSON.parse(readFileSync(constants.record, "utf-8"));
_buffer.githubid = detectedUser;
_buffer.signer = JSON.parse(
readFileSync(constants.verify, "utf-8")
).signer;
// Read from buffer
addr60[0].value = _buffer.records.address.eth;
avatar[0].value = _buffer.records.text.avatar;
contenthash[0].value =
!_buffer.records.contenthash ||
_buffer.records.contenthash === null
? constants.defaultContenthash
: _buffer.records.contenthash;
// addr60
if (_buffer.records.address.eth) {
const _file = await helper.createDeepFile(
constants.records.addr60
);
if (_file) {
writeFileSync(
constants.records.addr60,
JSON.stringify(addr60[0], null, 2)
);
} else {
resolve(false);
}
}
// avatar
if (_buffer.records.text.avatar) {
const _file = await helper.createDeepFile(
constants.records.avatar
);
if (_file) {
writeFileSync(
constants.records.avatar,
JSON.stringify(avatar[0], null, 2)
);
} else {
resolve(false);
}
}
// contenthash
if (_buffer.records.contenthash) {
const _file = await helper.createDeepFile(
constants.records.contenthash
);
if (_file) {
writeFileSync(
constants.records.contenthash,
JSON.stringify(contenthash[0], null, 2)
);
} else {
resolve(false);
}
}
if (
!_buffer.records.contenthash ||
_buffer.records.contenthash === null
) {
const _file = await helper.createDeepFile(
constants.records.contenthash
);
if (_file) {
writeFileSync(
constants.records.contenthash,
JSON.stringify(contenthash[0], null, 2)
);
} else {
resolve(false);
}
}
resolve(true);
}
);
});
} else if (welcome && written && written_contenthash) {
return new Promise(async (resolve) => {
rl.question(
` ▶ Please Confirm Records Update (press ENTER to confirm; CTRL + C to exit): `,
async (done) => {
if (!done) {
let _buffer = JSON.parse(readFileSync(constants.record, "utf-8"));
// Write to buffer
_buffer.githubid = detectedUser;
_buffer.signer = JSON.parse(
readFileSync(constants.verify, "utf-8")
).signer;
_buffer.records.address.eth = addr60[0].value;
_buffer.records.text.avatar = avatar[0].value;
_buffer.records.contenthash = contenthash[0].value;
writeFileSync(constants.record, JSON.stringify(_buffer, null, 2));
// addr60
if (_buffer.records.address.eth) {
const _file = await helper.createDeepFile(
constants.records.addr60
);
if (_file) {
writeFileSync(
constants.records.addr60,
JSON.stringify(addr60[0], null, 2)
);
} else {
resolve(false);
}
}
// avatar
if (_buffer.records.text.avatar) {
const _file = await helper.createDeepFile(
constants.records.avatar
);
if (_file) {
writeFileSync(
constants.records.avatar,
JSON.stringify(avatar[0], null, 2)
);
} else {
resolve(false);
}
}
// contenthash
if (_buffer.records.contenthash) {
const _file = await helper.createDeepFile(
constants.records.contenthash
);
if (_file) {
writeFileSync(
constants.records.contenthash,
JSON.stringify(contenthash[0], null, 2)
);
} else {
resolve(false);
}
}
resolve(true);
} else {
graphics.print(" ● Bad Input", "orange");
resolve(
await confirmRecords(
detectedUser,
written,
written_contenthash,
addr60,
avatar,
contenthash
)
); // Recursive call
}
}
);
});
}
}
// Verifies ENS Records
async function verifyRecords(welcome, confirmed) {
if (welcome && confirmed) {
return new Promise(async (resolve) => {
let __addr60 = { ...constants.recordContent };
let __avatar = { ...constants.recordContent };
let __contenthash = { ...constants.recordContent };
if (existsSync(constants.record)) {
let records = JSON.parse(readFileSync(constants.record, "utf-8"));
__addr60.value = records.records.address.eth || null;
__avatar.value = records.records.text.avatar || null;
__contenthash.value =
records.records.contenthash || constants.defaultContenthash;
}
// 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;
} else if (!__addr60.value || __addr60.value === null) {
flag.addr60 = true;
graphics.print(
" ○ Empty 'addr60:' value in 'records.json'",
"skyblue"
);
} else {
graphics.print(" ■ Bad 'addr60:' value in 'records.json'", "orange");
}
// avatar
if (
__avatar.value &&
__avatar.value !== null &&
helper.isAvatar(__avatar.value)
) {
flag.avatar = true;
} else if (!__avatar.value || __avatar.value === null) {
flag.avatar = true;
graphics.print(
" ○ Empty 'avatar:' value in 'records.json'",
"skyblue"
);
} else {
graphics.print(" ■ Bad 'avatar:' value in 'records.json'", "orange");
}
// contenthash
if (
__contenthash.value &&
__contenthash.value !== null &&
helper.isContenthash(__contenthash.value)
) {
flag.contenthash = true;
} else if (!__contenthash.value || __contenthash.value === null) {
flag.contenthash = true;
graphics.print(
" ○ Empty 'contenthash:' value in 'records.json'",
"skyblue"
);
} else {
graphics.print(
" ■ Bad 'contenthash:' value in 'records.json'",
"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);
}
});
} else {
return new Promise(async (resolve) => {
graphics.print(
` ■ Records failed to write due to unknown reason!`,
"orange"
);
graphics.print(` ⨯ Quitting...`, "orange");
resolve(false);
rl.close();
});
}
}
// Signs ENS Records
async function signRecords(
detectedUser,
record,
type,
key,
resolver,
welcome,
verified
) {
if (welcome && record) {
if (verified) {
return new Promise(async (resolve) => {
graphics.print(` ○ Signing Record: ${type}`, "skyblue");
const _signed = helper.signRecord(
`https://${detectedUser}.github.io`,
constants.NETWORK === "mainnet" ? "1" : "5",
resolver,
type,
helper.genExtradata(key, record),
JSON.parse(readFileSync(constants.verify, "utf-8")).signer
);
resolve(_signed);
});
} else {
return new Promise(async (resolve) => {
graphics.print(
` ⨯ Please fix the ${type} record & then re-try \'npm run publish\'. Quitting...`,
"orange"
);
resolve([null, null]);
rl.close();
});
}
} else {
return new Promise(async (resolve) => {
graphics.print(
` ▽ Skipping Record: ${type}`,
welcome ? "skyblue" : "orange"
);
resolve([null, null]);
});
}
}
// Gets status of CF approval
async function getStatus(detectedUser, welcome, payload) {
if (welcome) {
return new Promise(async (resolve) => {
let _verify = JSON.parse(readFileSync(constants.verify, "utf-8"));
let _buffer = JSON.parse(readFileSync(constants.record, "utf-8"));
graphics.print(
` ○ Waiting for validation from Cloudflare...`,
"skyblue"
);
const _url = `${constants.validator}${detectedUser}`;
const response = await fetch(_url);
if (!response.ok) {
graphics.print(
` ■ Failed to connect to Cloudflare validator: error ${response.status}`,
"orange"
);
graphics.print(` ⨯ Quitting...`, "orange");
rl.close();
resolve(false);
}
const verifier = await response.json();
if (
verifier.gateway === `${detectedUser}.github.io` &&
verifier.approvedFor === _verify.signer
) {
_verify.verified = true;
_verify.accessKey = verifier.approvalSig;
_buffer.approval = verifier.approvalSig;
graphics.print(
` ✓ Validated Signer: ${_verify.signer}`,
"lightgreen"
);
graphics.print(
` ○ Writing records to .well-known/eth/dev3/${detectedUser}...`,
"skyblue"
);
// addr60
if (_buffer.records.address.eth) {
let _addr60 = JSON.parse(
readFileSync(constants.records.addr60, "utf-8")
);
_addr60.data = helper.encodeValue(
"address",
_addr60.value,
_verify.signer,
signature_addr60,
verifier.approvalSig
);
_addr60.signer = _verify.signer;
_addr60.payload = payload.addr60;
_addr60.signature = signature_addr60;
_addr60.approved = true;
_addr60.approval = verifier.approvalSig;
writeFileSync(
constants.records.addr60,
JSON.stringify(_addr60, null, 2)
);
}
// avatar
if (_buffer.records.text.avatar) {
let _avatar = JSON.parse(
readFileSync(constants.records.avatar, "utf-8")
);
_avatar.data = helper.encodeValue(
"avatar",
_avatar.value,
_verify.signer,
signature_avatar,
verifier.approvalSig
);
_avatar.signer = _verify.signer;
_avatar.payload = payload.avatar;
_avatar.signature = signature_avatar;
_avatar.approved = true;
_avatar.approval = verifier.approvalSig;
writeFileSync(
constants.records.avatar,
JSON.stringify(_avatar, null, 2)
);
}
// contenthash
if (_buffer.records.contenthash || constants.defaultContenthash) {
let _contenthash = JSON.parse(
readFileSync(constants.records.contenthash, "utf-8")
);
_contenthash.data = helper.encodeValue(
"contenthash",
_contenthash.value,
_verify.signer,
signature_contenthash,
verifier.approvalSig
);
_contenthash.signer = _verify.signer;
_contenthash.payload = payload.contenthash;
_contenthash.signature = signature_contenthash;
_contenthash.approved = true;
_contenthash.approval = verifier.approvalSig;
writeFileSync(
constants.records.contenthash,
JSON.stringify(_contenthash, null, 2)
);
}
} else {
graphics.print(
` ■ Cloudflare validation failed: Signer DOES NOT match!`,
"orange"
);
graphics.print(` ⨯ Quitting...`, "orange");
rl.close();
resolve(false);
}
writeFileSync(constants.verify, JSON.stringify(_verify, null, 2));
writeFileSync(constants.record, JSON.stringify(_buffer, null, 2));
let _container = `.well-known/eth/dev3/${detectedUser}`;
if (existsSync(_container)) {
// Delete .well-known/eth/dev3/user/
execSync(`rm -r ${_container}/*`);
} else {
// Create .well-known/eth/dev3/user/
execSync(`mkdir -p ${_container}`);
}
// Copy Records to .well-known
if (existsSync("records")) {
execSync(`cp -r records/* ${_container} && rm -r records`);
}
resolve(true);
});
} else {
return new Promise(async (resolve) => {
graphics.print(` ⨯ Quitting...`, "orange");
rl.close();
resolve(false);
});
}
}
// CLI
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// WELCOME!
console.log();
graphics.print(graphics.asciiArt, "orange");
graphics.logo();
graphics.print(graphics.signAsciiArt, "orange");
console.log();
// Check Git Repository
const [isGitRepo, detectedUser, branch, githubKey, synced, status] =
await helper.validateGitRepo(rl);
let userDetected = undefined;
if (isGitRepo && detectedUser && synced) {
userDetected = await helper.requestGithubID(detectedUser, rl);
} else {
graphics.print(` ⨯ Quitting...`, "orange");
rl.close();
}
const welcome = synced
? userDetected
? await helper.skipGithubID(detectedUser, constants.verify)
: await helper.validateGithubID(rl, constants.verify)
: false;
// MAIN ============================================
// Define Records
let addr60 = [
{ ...constants.recordContent },
`.well-known/eth/dev3/${detectedUser}/address/60.json`,
];
let avatar = [
{ ...constants.recordContent },
`.well-known/eth/dev3/${detectedUser}/text/avatar.json`,
];
let contenthash = [
{ ...constants.recordContent },
`.well-known/eth/dev3/${detectedUser}/contenthash.json`,
];
/* Define more ENS Records here */
let written = await writeRecords(welcome);
let [written_addr60, _addr60] = await write_addr60(addr60, welcome, written);
addr60 = _addr60;
let [written_avatar, _avatar] = await write_avatar(
avatar,
welcome,
written,
written_addr60
);
avatar = _avatar;
let [written_contenthash, _contenthash] = await write_contenthash(
contenthash,
welcome,
written,
written_addr60,
written_avatar
);
contenthash = _contenthash;
let confirmed = await confirmRecords(
detectedUser,
written,
written_contenthash,
addr60,
avatar,
contenthash
);
const verified = await verifyRecords(welcome, confirmed);
// Sign addr60
const [payload_addr60, signature_addr60] = await signRecords(
detectedUser,
JSON.parse(readFileSync(constants.record, "utf-8")).records.address.eth,
"address/60",
"address",
constants.resolver,
welcome,
verified
);
// Sign avatar
const [payload_avatar, signature_avatar] = await signRecords(
detectedUser,
JSON.parse(readFileSync(constants.record, "utf-8")).records.text.avatar,
"text/avatar",
"avatar",
constants.resolver,
welcome,
verified
);
// Sign contenthash
const [payload_contenthash, signature_contenthash] = await signRecords(
detectedUser,
JSON.parse(readFileSync(constants.record, "utf-8")).records.contenthash ||
constants.defaultContenthash,
"contenthash",
"contenthash",
constants.resolver,
welcome,
verified
);
const payload = {
addr60: payload_addr60,
avatar: payload_avatar,
contenthash: payload_contenthash,
};
const validated = await getStatus(detectedUser, welcome, payload);
let success = await helper.gitCommitPush(
status,
validated,
branch,
githubKey,
detectedUser,
rl,
"verify.json .gitignore .nojekyll .well-known index.htm* records*",
`🎉 Successfully updated ENS Records with dev3.eth! To check your signed ENS Records for \'${detectedUser}.dev3.eth\', try \'npx dev3-eth view\' OR \'npm run view\'`
);
if (!success) rl.close();
}