*Initial version

This commit is contained in:
Derrick Hammer 2023-03-28 07:35:31 -04:00
parent 9ffde1963d
commit fd7e773752
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
10 changed files with 469 additions and 4 deletions

20
LICENSE
View File

@ -1,9 +1,21 @@
MIT License MIT License
Copyright (c) <year> <copyright holders> Copyright (c) 2022 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
build.js Normal file
View File

@ -0,0 +1,15 @@
import esbuild from "esbuild";
esbuild.buildSync({
entryPoints: ["src-module/index.ts"],
outfile: "dist-module/index.js",
format: "esm",
bundle: true,
legalComments: "external",
// minify: true
inject: ["./polyfill.js"],
tsconfig: "tsconfig.module.json",
define: {
global: "self",
},
});

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@lumeweb/resolver-module-eip137",
"version": "0.1.0",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build-script": "tsc --project tsconfig.build.json && mv dist-build/build.js dist-build/build.mjs",
"compile-module": "npm run build-script && node build.js",
"build": "tsc --project tsconfig.json",
"build-module": "npm run compile-module && node ./dist-build/build.mjs dev"
},
"dependencies": {
"@lumeweb/ensjs": "^2.1.2",
"@lumeweb/kernel-eth-client": "git+https://git.lumeweb.com/LumeWeb/kernel-eth-client.git",
"@lumeweb/kernel-libresolver": "git+https://git.lumeweb.com/LumeWeb/kernel-libresolver.git",
"@lumeweb/libresolver": "git+https://git.lumeweb.com/LumeWeb/libresolver.git",
"ethers": "^6.2.3",
"libskynet": "^0.1.9"
},
"devDependencies": {
"@scure/bip39": "^1.2.0",
"@skynetlabs/skynet-nodejs": "^2.9.0",
"@types/node": "^18.15.10",
"@types/read": "^0.0.29",
"buffer": "^6.0.3",
"cli-progress": "^3.12.0",
"crypto-browserify": "^3.12.0",
"esbuild": "^0.15.18",
"libskynetnode": "^0.1.4",
"os-browserify": "^0.3.0",
"prettier": "^2.8.7",
"process": "^0.11.10",
"read": "^1.0.7",
"stream-browserify": "^3.0.0",
"typescript": "^5.0.2"
},
"browser": {
"crypto": "crypto-browserify",
"stream": "stream-browserify",
"os": "os-browserify"
}
}

2
polyfill.js Normal file
View File

@ -0,0 +1,2 @@
export const process = require("process");
export const Buffer = require("buffer").Buffer;

218
src-build/build.ts Normal file
View File

@ -0,0 +1,218 @@
// This is the standard build script for a kernel module.
import * as fs from "fs";
import read from "read";
import * as bip39 from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
//@ts-ignore
import { SkynetClient } from "@skynetlabs/skynet-nodejs";
// Helper variables to make it easier to return empty values alongside errors.
const nu8 = new Uint8Array(0);
const nkp = {
publicKey: nu8,
secretKey: nu8,
};
// readFile is a wrapper for fs.readFileSync that handles the try-catch for the
// caller.
function readFile(fileName: string): [string, string | null] {
try {
let data = fs.readFileSync(fileName, "utf8");
return [data, null];
} catch (err) {
return ["", "unable to read file: " + JSON.stringify(err)];
}
}
// readFileBinary is a wrapper for fs.readFileSync that handles the try-catch
// for the caller.
function readFileBinary(fileName: string): [Uint8Array, string | null] {
try {
let data = fs.readFileSync(fileName, null);
return [data, null];
} catch (err) {
return [nu8, "unable to read file: " + JSON.stringify(err)];
}
}
// writeFile is a wrapper for fs.writeFileSync which handles the try-catch in a
// non-exception way.
function writeFile(fileName: string, fileData: string): string | null {
try {
fs.writeFileSync(fileName, fileData);
return null;
} catch (err) {
return "unable to write file: " + JSON.stringify(err);
}
}
// handlePass handles all portions of the script that occur after the password
// has been requested. If no password needs to be requested, handlePass will be
// called with a null input. We need to structure the code this way because the
// password reader is async and we can only access the password when using a
// callback.
function handlePass(password: string) {
try {
// If we are running prod and the seed file does not exist, we
// need to confirm the password and also warn the user to use a
// secure password.
if (!fs.existsSync(seedFile) && process.argv[2] === "prod") {
// The file does not exist, we need to confirm the
// password.
console.log();
console.log(
"No production entry found for module. Creating new production module..."
);
console.log(
"If someone can guess the password, they can push arbitrary changes to your module."
);
console.log("Please use a secure password.");
console.log();
read(
{ prompt: "Confirm Password: ", silent: true },
function (err: any, confirmPassword: string) {
if (err) {
console.error("unable to fetch password:", err);
process.exit(1);
}
if (password !== confirmPassword) {
console.error("passwords do not match");
process.exit(1);
}
handlePassConfirm(moduleSalt, password);
}
);
} else {
// If the seed file does exist, or if we are using dev,
// there's no need to confirm the password but we do
// need to pass the logic off to the handlePassConfirm
// callback.
handlePassConfirm(moduleSalt, password);
}
} catch (err) {
console.error("Unable to read seedFile:", err);
process.exit(1);
}
}
// handlePassConfirm handles the full script after the confirmation password
// has been provided. If not confirmation password is needed, this function
// will be called anyway using the unconfirmed password as input.
function handlePassConfirm(seed: string, password: string) {
// Create the seedFile if it does not exist. For dev we just save the
// seed to disk outright, because this is a dev build and therefore not
// security sensitive. Also the dev seed does not get pushed to the
// github repo.
//
// For prod, we use the seed to create a new seed (called the shield)
// which allows us to verify that the developer has provided the right
// password when deploying the module. The shield does get pushed to
// the github repo so that the production module is the same on all
// devices.
if (!fs.existsSync(seedFile) && process.argv[2] !== "prod") {
// Generate the seed phrase and write it to the file.
let seedPhrase = bip39.generateMnemonic(wordlist);
let errWF = writeFile(seedFile, seedPhrase);
if (errWF !== null) {
console.error("unable to write file:", errWF);
process.exit(1);
}
} else if (!fs.existsSync(seedFile) && process.argv[2] === "prod") {
// Generate the seed phrase.
let seedPhrase = bip39.generateMnemonic(wordlist);
// Write the registry link to the file.
}
// Load or verify the seed. If this is prod, the password is used to
// create and verify the seed. If this is dev, we just load the seed
// with no password.
let seedPhrase: string;
let registryLink: string;
if (process.argv[2] === "prod") {
// Generate the seed phrase from the password.
seedPhrase = bip39.generateMnemonic(wordlist);
} else {
let [sp, errRF] = readFile(seedFile);
if (errRF !== null) {
console.error("unable to read seed phrase for dev command from disk");
process.exit(1);
}
seedPhrase = sp;
}
let metadata = {
Filename: "index.js",
};
const client = new SkynetClient("https://web3portal.com");
client
.uploadFile("dist-module/index.js")
.then((result: any) => {
console.log("Immutable Link for kernel:", result);
})
.catch((err: any) => {
console.error("unable to upload file", err);
process.exit(1);
});
}
// Add a newline for readability.
console.log();
// Check for a 'dev' or 'prod' input to the script.
if (process.argv.length !== 3) {
console.error("need to provide either 'dev' or 'prod' as an input");
process.exit(1);
}
// Create the build folder if it does not exist.
if (!fs.existsSync("build")) {
fs.mkdirSync("build");
}
// Determine the seed file.
let seedFile: string;
if (process.argv[2] === "prod") {
seedFile = "module-skylink";
} else if (process.argv[2] === "dev") {
seedFile = "build/dev-seed";
} else {
console.error("need to provide either 'dev' or 'prod' as an input");
process.exit(1);
}
// If doing a prod deployment, check whether the salt file exists. If it does
// not, create it.
let moduleSalt: string;
if (!fs.existsSync(".module-salt")) {
moduleSalt = bip39.generateMnemonic(wordlist);
let errWF = writeFile(".module-salt", moduleSalt);
if (errWF !== null) {
console.error("unable to write module salt file:", errWF);
process.exit(1);
}
} else {
let [ms, errRF] = readFile(".module-salt");
if (errRF !== null) {
console.error("unable to read moduleSalt");
process.exit(1);
}
ms = ms.replace(/\n$/, "");
moduleSalt = ms;
}
// Need to get a password if this is a prod build.
if (process.argv[2] === "prod") {
read(
{ prompt: "Password: ", silent: true },
function (err: any, password: string) {
if (err) {
console.error("unable to fetch password:", err);
process.exit(1);
}
handlePass(password);
}
);
} else {
handlePass("");
}

5
src-module/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { setup } from "@lumeweb/kernel-libresolver";
import Eth from "../src/index.js";
// @ts-ignore
setup(new Eth());

134
src/index.ts Normal file
View File

@ -0,0 +1,134 @@
import {
AbstractResolverModule,
DNS_RECORD_TYPE,
DNSRecord,
DNSResult,
isPromise,
resolverEmptyResponse,
resolverError,
ResolverOptions,
resolveSuccess,
} from "@lumeweb/libresolver";
import { ethers } from "ethers";
import { createProvider } from "@lumeweb/kernel-eth-client";
// @ts-ignore
import ENSRoot, { getEnsAddress } from "@lumeweb/ensjs";
const ENS = ENSRoot.default;
const ETH_PROVIDER = createProvider();
// @ts-ignore
ETH_PROVIDER._isProvider = true;
const ETH_SIGNER = new ethers.VoidSigner(ethers.ZeroAddress, ETH_PROVIDER);
// @ts-ignore
ETH_SIGNER._isSigner = true;
// @ts-ignore
ETH_PROVIDER.getSigner = () => ETH_SIGNER;
export default class Eth extends AbstractResolverModule {
// @ts-ignore
getSupportedTlds(): string[] {
return ["eth"];
}
async resolve(
domain: string,
options: ResolverOptions,
bypassCache: boolean
): Promise<DNSResult> {
const hip5Data = domain.split(".");
if (
2 <= hip5Data.length &&
options.options &&
"domain" in options.options
) {
if (ethers.isAddress(hip5Data[0])) {
let chain = hip5Data[1].replace("_", "");
domain = options.options.domain;
if (chain !== "eth") {
return resolverError("HIP5 chain not supported");
}
return this.resolve137(domain, options, bypassCache);
}
}
if (await this.isTldSupported(domain)) {
return this.resolve137(domain, options, bypassCache);
}
return resolverEmptyResponse();
}
private async resolve137(
domain: string,
options: ResolverOptions,
bypassCache: boolean
): Promise<DNSResult> {
const records: DNSRecord[] = [];
const ens = new ENS({
provider: ETH_PROVIDER,
ensAddress: getEnsAddress(1),
});
let name;
try {
name = await ens.name(domain);
} catch (e: any) {
return resolverError(e);
}
let content;
if (
[DNS_RECORD_TYPE.CONTENT, DNS_RECORD_TYPE.TEXT].includes(options.type)
) {
try {
content = maybeGetContentHash(await name.getContent());
} catch (e: any) {
return resolverError(e);
}
records.push({
type: DNS_RECORD_TYPE.CONTENT,
value: content as string,
});
}
if ([DNS_RECORD_TYPE.CUSTOM].includes(options.type)) {
let text;
try {
text = await name.getText(options.customType);
} catch (e: any) {
return resolverError(e);
}
records.push({
type: options.type,
customType: options.customType,
value: content as string,
});
}
if (0 < records.length) {
return resolveSuccess(records);
}
return resolverEmptyResponse();
}
}
export function maybeGetContentHash(contentResult: any): string | boolean {
let content = false;
if (
typeof contentResult === "object" &&
"contenthash" === contentResult.contentType
) {
content = contentResult.value;
}
return content;
}

13
tsconfig.build.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2021",
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"declaration": true,
"outDir": "./dist-build",
"strict": true
},
"include": ["src-build"],
"exclude": ["node_modules", "**/__tests__/*"]
}

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2021",
"declaration": true,
"moduleResolution": "nodenext",
"outDir": "./dist",
"strict": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

12
tsconfig.module.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2021",
"declaration": true,
"moduleResolution": "node",
"outDir": "./dist-module",
"strict": true,
"allowSyntheticDefaultImports": true
},
"include": ["src-module"],
"exclude": ["node_modules", "**/__tests__/*"]
}