setting up

This commit is contained in:
thatben 2025-04-21 15:03:43 +02:00
parent a63a8944a2
commit d017b5a733
No known key found for this signature in database
GPG Key ID: 62C543548433D43E
13 changed files with 296 additions and 26 deletions

115
package-lock.json generated
View File

@ -12,6 +12,8 @@
"axios": "^1.6.2",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"crypto": "^1.0.1",
"ethers": "^6.13.5",
"fs-extra": "^11.3.0",
"fs-filesystem": "^2.1.2",
"inquirer": "^9.2.12",
@ -31,6 +33,12 @@
"node": ">=20"
}
},
"node_modules/@adraffy/ens-normalize": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
@ -446,6 +454,30 @@
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
@ -699,6 +731,15 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@vitest/expect": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz",
@ -805,6 +846,12 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/aes-js": {
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
"license": "MIT"
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -1212,6 +1259,13 @@
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
"license": "ISC"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -1364,6 +1418,40 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/ethers": {
"version": "6.13.5",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz",
"integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/ethers-io/"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "1.10.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.2",
"@types/node": "22.7.5",
"aes-js": "4.0.0-beta.5",
"tslib": "2.7.0",
"ws": "8.17.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ethers/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/expect-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
@ -2237,6 +2325,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@ -2571,6 +2665,27 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yoctocolors-cjs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",

View File

@ -27,6 +27,8 @@
"axios": "^1.6.2",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"crypto": "^1.0.1",
"ethers": "^6.13.5",
"fs-extra": "^11.3.0",
"fs-filesystem": "^2.1.2",
"inquirer": "^9.2.12",
@ -34,7 +36,7 @@
"nanospinner": "^1.1.0",
"open": "^10.1.0",
"ps-list": "^8.1.1"
},
},
"devDependencies": {
"prettier": "^3.4.2",
"vitest": "^3.0.5"

View File

@ -30,6 +30,7 @@ export const mockFsService = {
moveDir: vi.fn(),
deleteDir: vi.fn(),
readJsonFile: vi.fn(),
readFile: vi.fn(),
writeJsonFile: vi.fn(),
writeFile: vi.fn(),
ensureDirExists: vi.fn(),

View File

@ -1,10 +1,17 @@
export class Installer {
constructor(configService, shellService, osService, fsService) {
constructor(
configService,
shellService,
osService,
fsService,
marketplaceSetup,
) {
this.config = configService.get();
this.configService = configService;
this.shell = shellService;
this.os = osService;
this.fs = fsService;
this.market = marketplaceSetup;
}
isCodexInstalled = async () => {
@ -28,6 +35,8 @@ export class Installer {
this.fs.ensureDirExists(this.config.codexRoot);
if (!(await this.arePrerequisitesCorrect(processCallbacks))) return;
if (!(await this.market.runClientWizard())) return;
processCallbacks.installStarts();
if (this.os.isWindows()) {
await this.installCodexWindows(processCallbacks);

View File

@ -72,6 +72,13 @@ export class ProcessControl {
const workingDir = this.configService.get().codexRoot;
const args = [
`--config-file=${this.configService.getCodexConfigFilePath()}`,
// Marketplace client parameters cannot be set via config file.
// Open issue: https://github.com/codex-storage/nim-codex/issues/1206
// So we're setting them here.
"persistence",
`--eth-provider=https://rpc.testnet.codex.storage`,
`--eth-private-key=eth.key`
];
await this.shell.spawnDetachedProcess(executable, workingDir, args);
};

View File

@ -20,7 +20,6 @@ import {
import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js";
import { showInfoMessage } from "./utils/messages.js";
import { ConfigService } from "./services/configService.js";
import { UiService } from "./services/uiService.js";
import { FsService } from "./services/fsService.js";
import { MainMenu } from "./ui/mainMenu.js";
@ -35,6 +34,8 @@ import { OsService } from "./services/osService.js";
import { ProcessControl } from "./handlers/processControl.js";
import { CodexGlobals } from "./services/codexGlobals.js";
import { CodexApp } from "./services/codexApp.js";
import { EthersService } from "./services/ethersService.js";
import { MarketplaceSetup } from "./ui/marketplaceSetup.js";
async function showNavigationMenu() {
console.log("\n");
@ -108,11 +109,18 @@ export async function main() {
const configService = new ConfigService(fsService, osService);
const codexApp = new CodexApp(configService);
const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService);
const ethersService = new EthersService(fsService, configService);
const marketplaceSetup = new MarketplaceSetup(
uiService,
configService,
ethersService,
);
const installer = new Installer(
configService,
shellService,
osService,
fsService,
marketplaceSetup,
);
const installMenu = new InstallMenu(
uiService,

View File

@ -10,4 +10,8 @@ export class CodexGlobals {
const result = (await axios.get(`https://spr.codex.storage/testnet`)).data;
return result.split("\n").filter((line) => line.length > 0);
};
getEthProvider = () => {
return "https://rpc.testnet.codex.storage";
}
}

View File

@ -13,6 +13,8 @@ const defaultConfig = {
const datadir = "datadir";
const codexLogFile = "codex.log";
const codexConfigFile = "config.toml";
const ethKeyFile = "eth.key";
const ethAddressFile = "eth.address";
export class ConfigService {
constructor(fsService, osService) {
@ -38,6 +40,13 @@ export class ConfigService {
return this.fs.pathJoin([this.config.codexRoot, codexConfigFile]);
};
getEthFilePaths = () => {
return {
key: this.fs.pathJoin([this.config.codexRoot, ethKeyFile]),
address: this.fs.pathJoin([this.config.codexRoot, ethAddressFile]),
};
};
loadConfig = () => {
const filePath = this.getConfigFilename();
try {
@ -76,7 +85,7 @@ export class ConfigService {
throw new Error("Storage quota must be at least 100MB");
};
writeCodexConfigFile = (publicIp, bootstrapNodes) => {
writeCodexConfigFile = (publicIp, bootstrapNodes, ethProvider) => {
this.validateConfiguration();
const nl = "\n";
@ -85,15 +94,22 @@ export class ConfigService {
this.fs.writeFile(
this.getCodexConfigFilePath(),
`data-dir="${datadir}"${nl}` +
`log-level="DEBUG"${nl}` +
`log-file="${codexLogFile}"${nl}` +
`storage-quota=${this.config.storageQuota}${nl}` +
`disc-port=${this.config.ports.discPort}${nl}` +
`listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` +
`api-port=${this.config.ports.apiPort}${nl}` +
`nat="extip:${publicIp}"${nl}` +
`api-cors-origin="*"${nl}` +
`bootstrap-node=[${bootNodes}]${nl}`,
`log-level="TRACE"${nl}` +
`log-file="${codexLogFile}"${nl}` +
`storage-quota=${this.config.storageQuota}${nl}` +
`disc-port=${this.config.ports.discPort}${nl}` +
`listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` +
`api-port=${this.config.ports.apiPort}${nl}` +
`nat="extip:${publicIp}"${nl}` +
`api-cors-origin="*"${nl}` +
`bootstrap-node=[${bootNodes}]${nl}` +
// Marketplace client parameters:
// `[persistence]${nl}` +
//`eth-provider="${ethProvider}"${nl}` +
// `eth-provider="https://rpc.testnet.codex.storage"${nl}` +
// //`eth-private-key="${ethKeyFile}"${nl}` +
// `eth-private-key="notafile.no"${nl}` +
`${nl}`
);
};
}

View File

@ -0,0 +1,41 @@
import { ethers } from 'ethers';
import crypto from "crypto";
export class EthersService {
constructor(fsService, configService) {
this.fs = fsService;
this.configService = configService;
}
getOrCreateEthKey = () => {
const paths = this.configService.getEthFilePaths();
if (!this.fs.isFile(paths.key)) {
this.generateAndSaveKey(paths);
}
const address = this.fs.readFile(paths.address);
return {
privateKeyFilePath: paths.key,
addressFilePath: paths.address,
address: address,
};
};
generateAndSaveKey = async (paths) => {
const keys = this.generateKey();
this.fs.writeFile(paths.key, keys.key);
this.fs.writeFile(paths.address, keys.address);
};
generateKey = () => {
var id = crypto.randomBytes(32).toString("hex");
var privateKey = "0x" + id;
var wallet = new ethers.Wallet(privateKey);
return {
key: privateKey,
address: wallet.address,
};
};
}

View File

@ -68,6 +68,10 @@ export class FsService {
return JSON.parse(fs.readFileSync(filePath));
};
readFile = (filePath) => {
return fs.readFileSync(filePath);
};
writeJsonFile = (filePath, jsonObject) => {
fs.writeFileSync(filePath, JSON.stringify(jsonObject));
};

View File

@ -20,20 +20,20 @@ export class ShellService {
var child = spawn(cmd, args, {
cwd: workingDir,
detached: true,
stdio: ["ignore", "ignore", "ignore"],
//stdio: ["ignore", "ignore", "ignore"],
});
// child.stdout.on("data", (data) => {
// console.log(`stdout: ${data}`);
// });
child.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});
// child.stderr.on("data", (data) => {
// console.error(`stderr: ${data}`);
// });
child.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
// child.on("close", (code) => {
// console.log(`child process exited with code ${code}`);
// });
child.on("close", (code) => {
console.log(`child process exited with code ${code}`);
});
child.unref();

View File

@ -91,12 +91,12 @@ export class MainMenu {
};
startCodex = async () => {
const spinner = this.ui.createAndStartSpinner("Starting...");
// const spinner = this.ui.createAndStartSpinner("Starting...");
try {
await this.processControl.startCodexProcess();
this.ui.stopSpinnerSuccess(spinner);
// this.ui.stopSpinnerSuccess(spinner);
} catch (exception) {
this.ui.stopSpinnerError(spinner);
// this.ui.stopSpinnerError(spinner);
this.ui.showErrorMessage(`Failed to start Codex. "${exception}"`);
}
};

View File

@ -0,0 +1,63 @@
const ethFaucetAddress = "https://faucet-eth.testnet.codex.storage/";
const tstFaucetAddress = "https://faucet-tst.testnet.codex.storage/";
const discordServerAddress = "https://discord.gg/codex-storage";
const botChannelLink =
"https://discord.com/channels/895609329053474826/1230785221553819669";
export class MarketplaceSetup {
constructor(uiService, configService, ethersService) {
this.ui = uiService;
this.ethers = ethersService;
this.config = configService.get();
}
runClientWizard = async () => {
await this.generateKeyPair();
await this.showMintInstructions();
return this.isSuccessful;
};
generateKeyPair = async () => {
const ehtKey = await this.ethers.getOrCreateEthKey();
this.ui.showSuccessMessage(
"Your Codex node Ethereum account:\n" +
`Private key saved to '${ehtKey.privateKeyFilePath}'\n` +
`Address saved to '${ehtKey.addressFilePath}'\n` +
`Ethereum Account: '${ehtKey.address}'`,
);
};
showMintInstructions = async () => {
this.ui.showInfoMessage(
"Use one of these two methods to receive your testnet tokens:\n\n" +
"Faucets:\n" +
`Use the Eth faucet: '${ethFaucetAddress}'\n` +
`Then use the TST faucet: '${tstFaucetAddress}'\n\n` +
"or\n\n" +
"Discord bot:\n" +
`Join the server: ${discordServerAddress}\n` +
`Go to the #BOT channel: ${botChannelLink}\n` +
"Use '/set' and '/mint' commands to receive tokens.\n",
);
await this.ui.askMultipleChoice("Take your time.", [
{
label: "Proceed",
action: this.proceed,
},
{
label: "Abort",
action: this.abort,
},
]);
};
proceed = async () => {
this.isSuccessful = true;
};
abort = async () => {
this.isSuccessful = false;
};
}