From d017b5a733bc920fcd5e925d9d72666a963752c7 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 15:03:43 +0200 Subject: [PATCH] setting up --- package-lock.json | 115 +++++++++++++++++++++++++++++++++ package.json | 4 +- src/__mocks__/service.mocks.js | 1 + src/handlers/installer.js | 11 +++- src/handlers/processControl.js | 7 ++ src/main.js | 10 ++- src/services/codexGlobals.js | 4 ++ src/services/configService.js | 36 ++++++++--- src/services/ethersService.js | 41 ++++++++++++ src/services/fsService.js | 4 ++ src/services/shellService.js | 20 +++--- src/ui/mainMenu.js | 6 +- src/ui/marketplaceSetup.js | 63 ++++++++++++++++++ 13 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 src/services/ethersService.js create mode 100644 src/ui/marketplaceSetup.js diff --git a/package-lock.json b/package-lock.json index bbf68dd..fa1a663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 95ff60b..ae2683b 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 89f730f..422636b 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -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(), diff --git a/src/handlers/installer.js b/src/handlers/installer.js index dffe6ec..4a561c8 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -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); diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index c6b7fbd..9655621 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -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); }; diff --git a/src/main.js b/src/main.js index 9adb505..4dfc13a 100644 --- a/src/main.js +++ b/src/main.js @@ -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, diff --git a/src/services/codexGlobals.js b/src/services/codexGlobals.js index b5bd3dc..d3344f2 100644 --- a/src/services/codexGlobals.js +++ b/src/services/codexGlobals.js @@ -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"; + } } diff --git a/src/services/configService.js b/src/services/configService.js index 73627e6..c287fa2 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -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}` ); }; } diff --git a/src/services/ethersService.js b/src/services/ethersService.js new file mode 100644 index 0000000..abb1352 --- /dev/null +++ b/src/services/ethersService.js @@ -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, + }; + }; +} diff --git a/src/services/fsService.js b/src/services/fsService.js index 8c91f33..7913e72 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -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)); }; diff --git a/src/services/shellService.js b/src/services/shellService.js index 6fde68e..b4c7f18 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -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(); diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 8dcb365..e58f587 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -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}"`); } }; diff --git a/src/ui/marketplaceSetup.js b/src/ui/marketplaceSetup.js new file mode 100644 index 0000000..40aee2b --- /dev/null +++ b/src/ui/marketplaceSetup.js @@ -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; + }; +}