diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml new file mode 100644 index 0000000..5259e81 --- /dev/null +++ b/.github/workflows/pack.yml @@ -0,0 +1,80 @@ +name: CLI publish + +on: + push: + tags: + - "v*" + +permissions: + contents: write + actions: write + packages: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: npm install + + - name: Build project + run: npm run build:cli + + - name: Zip files [linux] + run: zip -j cli/linux.zip cli/mineral-linux cli/README.md + + - name: Zip files [win] + run: zip -j cli/windows.zip cli/mineral-win.exe cli/README.md + + - name: Zip files [macos] + run: zip -j cli/macos.zip cli/mineral-macos cli/README.md + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset [win] + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./cli/windows.zip + asset_name: windows.zip + asset_content_type: application/zip + + - name: Upload Release Asset [linux] + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./cli/linux.zip + asset_name: linux.zip + asset_content_type: application/zip + + - name: Upload Release Asset [macos] + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./cli/macos.zip + asset_name: macos.zip + asset_content_type: application/zip diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..71604ca --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,176 @@ +/* eslint-disable fp/no-loops, fp/no-mutation, fp/no-mutating-methods, fp/no-let, no-constant-condition */ + +import { program } from "commander"; +import { + getProof, + formatBig, + runner, + getOrCreateMiner, + fetchBus, + CONFIG, +} from "../common"; +import { Config, MINE } from "../codegen/mineral/mine/structs"; +import { Miner } from "../codegen/mineral/miner/structs"; +import { decodeSuiPrivateKey } from "@mysten/sui.js/cryptography"; +import { SuiClient, getFullnodeUrl } from "@mysten/sui.js/client"; +import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519"; +import { SUI_TYPE_ARG, SUI_DECIMALS } from "@mysten/sui.js/utils"; +import chalk from "chalk"; + +const { WALLET, RPC } = process.env; + +const START_TIME = 1715534935000; +const USAGE_GUIDE = + "https://github.com/ronanyeah/mineral-app/blob/master/cli/README.md"; +const SETUP_PROMPT = + "Wallet not found. Consult the setup guide: " + USAGE_GUIDE; + +const settings = (() => { + return { + wallet: (() => { + if (!WALLET) { + return null; + } + return Ed25519Keypair.fromSecretKey( + decodeSuiPrivateKey(WALLET).secretKey + ); + })(), + rpc: new SuiClient({ + url: RPC || getFullnodeUrl("mainnet"), + }), + }; +})(); + +program + .name("mineral") + .description("Mineral CLI Miner\nhttps://mineral.supply/") + .version("1.0.0"); + +program + .command("profile") + .description("View your mining stats") + .action((_options) => + (async () => { + if (!settings.wallet) { + return program.error(SETUP_PROMPT); + } + const pub = settings.wallet.toSuiAddress(); + console.log(chalk.green("Wallet:"), pub); + const minerAcct = await getProof(settings.rpc, pub); + if (minerAcct) { + console.log(chalk.green("Miner:"), minerAcct); + } + const results = await Promise.all([ + (async () => { + const bal = await settings.rpc.getBalance({ + owner: pub, + coinType: SUI_TYPE_ARG, + }); + const val = formatBig(BigInt(bal.totalBalance), SUI_DECIMALS); + return [`💧 Sui Balance: ${val} SUI`]; + })(), + (async () => { + const bal = await settings.rpc.getBalance({ + owner: pub, + coinType: MINE.$typeName, + }); + const val = formatBig(BigInt(bal.totalBalance), SUI_DECIMALS); + return [`⛏️ Mineral Balance: ${val} $MINE`]; + })(), + (async () => { + const proof = await getProof(settings.rpc, pub); + if (!proof) { + return []; + } + const miner = await Miner.fetch(settings.rpc, proof); + return [ + `💰 Lifetime rewards: ${formatBig(miner.totalRewards, 9)} $MINE`, + `🏭 Lifetime hashes: ${miner.totalHashes}`, + ]; + })(), + ]); + results.flat().forEach((val) => console.log(val)); + })().catch(console.error) + ); + +program + .command("stats") + .description("View global Mineral stats") + .action((_options) => + (async () => { + const config = await Config.fetch(settings.rpc, CONFIG); + const bus = await fetchBus(settings.rpc); + console.log( + "Total distributed rewards:", + Number(config.totalRewards) / 1_000_000_000, + "$MINE" + ); + console.log("Total hashes processed:", Number(config.totalHashes)); + console.log( + "Current reward rate:", + Number(bus.rewardRate) / 1_000_000_000, + "$MINE / hash" + ); + console.log("Current difficulty:", bus.difficulty); + })().catch(console.error) + ); + +program + .command("create-wallet") + .description("Create a new Sui wallet") + .action(async (_options) => { + const wallet = new Ed25519Keypair(); + console.log(chalk.green("Wallet created:"), wallet.toSuiAddress()); + console.log(chalk.red("Private key:"), wallet.getSecretKey()); + console.log(chalk.blue("Mineral CLI usage guide:"), USAGE_GUIDE); + }); + +program + .command("mine") + .description("Start mining ⛏️") + .action((_options) => + (async () => { + if (!settings.wallet) { + return program.error(SETUP_PROMPT); + } + const bal = await settings.rpc.getBalance({ + owner: settings.wallet.toSuiAddress(), + coinType: SUI_TYPE_ARG, + }); + if (Number(bal.totalBalance) < 0.1) { + console.log( + chalk.red("Low balance"), + "in wallet", + settings.wallet.toSuiAddress() + ); + console.log("Send some SUI to this wallet to enable mining."); + } + + if (Date.now() < START_TIME) { + return program.error("⚠️ Mining has not started yet!"); + } + + console.error( + chalk.green("Mining with wallet:"), + settings.wallet.toSuiAddress() + ); + const minerAccount = await getOrCreateMiner( + settings.wallet, + settings.rpc + ); + const bus = await fetchBus(settings.rpc); + + if (!minerAccount) { + return program.error("Miner account not created!"); + } + runner( + settings.rpc, + bus.difficulty, + settings.wallet, + minerAccount, + console.log + ); + })().catch(console.error) + ); + +program.parse(process.argv); diff --git a/webpack.cli.js b/webpack.cli.js new file mode 100644 index 0000000..48d5088 --- /dev/null +++ b/webpack.cli.js @@ -0,0 +1,31 @@ +const webpack = require("webpack"); +const { resolve } = require("path"); + +const outFolder = resolve("./cli"); + +module.exports = { + target: "node", + mode: "production", + entry: "./src/cli/index.ts", + output: { + filename: "index.js", + path: outFolder, + }, + stats: "normal", + infrastructureLogging: { + level: "warn", + }, + module: { + rules: [ + { + test: /\.ts$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".ts", ".js"], + }, + plugins: [new webpack.NoEmitOnErrorsPlugin()], +};