From fc9dc07d331677af8e88ada479d833757001abc8 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 19 Nov 2024 15:22:56 -0600 Subject: [PATCH] feat(ar-io cli): init join-network command --- bundle.mjs | 2 +- package.json | 2 + src/cli/cli.ts | 28 ++++++-- src/cli/commands/balance.ts | 10 ++- src/cli/commands/joinNetwork.ts | 120 ++++++++++++++++++++++++++++++++ src/cli/options.ts | 106 ++++++++++++++++++++++++---- src/cli/utils.ts | 92 ++++++++++++++++++------ src/types/io.ts | 9 ++- src/utils/base64.ts | 2 +- yarn.lock | 26 +++++++ 10 files changed, 348 insertions(+), 49 deletions(-) create mode 100644 src/cli/commands/joinNetwork.ts diff --git a/bundle.mjs b/bundle.mjs index 38812808..5ce8a04b 100644 --- a/bundle.mjs +++ b/bundle.mjs @@ -21,7 +21,7 @@ const bundle = async () => { }, }), ], - external: ['commander'], + external: ['commander', 'prompts'], tsconfig: './tsconfig.web.json', outfile: './bundles/web.bundle.min.js', }) diff --git a/package.json b/package.json index 31565c6a..69a00430 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@semantic-release/npm": "^11.0.3", "@trivago/prettier-plugin-sort-imports": "^4.2.0", "@types/node": "^20.12.12", + "@types/prompts": "^2.4.9", "@types/sinon": "^10.0.15", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^6.4.0", @@ -131,6 +132,7 @@ "commander": "^12.1.0", "eventemitter3": "^5.0.1", "plimit-lit": "^3.0.1", + "prompts": "^2.4.2", "winston": "^3.13.0", "zod": "^3.23.8" }, diff --git a/src/cli/cli.ts b/src/cli/cli.ts index bfc8c24d..f4f8bb4c 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -20,7 +20,12 @@ import { program } from 'commander'; import { version } from '../version.js'; import { balance } from './commands/balance.js'; -import { balanceOptions, globalOptions } from './options.js'; +import { joinNetwork } from './commands/joinNetwork.js'; +import { + balanceOptions, + globalOptions, + joinNetworkOptions, +} from './options.js'; import { makeCommand, runCommand } from './utils.js'; makeCommand({ @@ -31,9 +36,6 @@ makeCommand({ .version(version) .helpCommand(true); -// ar-io delegate-stake --wallet BAD_BOY_NO_ALLOW_WALLET --gateway permagate.io --stakeQty 1500 --unit IO - -// balance --address
or --wallet-file makeCommand({ name: 'balance', description: 'Get the balance of an address', @@ -42,12 +44,28 @@ makeCommand({ await runCommand(command, balance); }); -// join-network +makeCommand({ + name: 'join-network', + description: 'Join the AR.IO network', + options: joinNetworkOptions, +}).action(async (_, command) => { + await runCommand(command, joinNetwork); +}); // delegate-stake +// increase-operator-stake + +// decrease-operator-stake + // withdraw-stake +// update-gateway-settings + +// transfer + +// redelegate-stake + if ( process.argv[1].includes('bin/turbo') || // Running from global .bin process.argv[1].includes('cli/cli') // Running from source diff --git a/src/cli/commands/balance.ts b/src/cli/commands/balance.ts index 483187be..0eb40420 100644 --- a/src/cli/commands/balance.ts +++ b/src/cli/commands/balance.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { MIO_PER_IO } from '../../constants.js'; import { BalanceOptions } from '../options.js'; import { addressFromOptions, readIOFromOptions } from '../utils.js'; @@ -21,10 +22,15 @@ export async function balance(options: BalanceOptions) { const address = addressFromOptions(options); const result = await io.getBalance({ address }); + const formattedBalance = (result / MIO_PER_IO).toFixed(6); + const [integerPart, decimalPart] = formattedBalance.split('.'); + const IOBalanceWithCommas = + integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '.' + decimalPart; + const output = { address: address, - balance: result, - message: 'mIO Balance retrieved successfully', + mIOBalance: result, + message: `Provided address current has a balance of ${IOBalanceWithCommas} IO`, }; console.log(output); diff --git a/src/cli/commands/joinNetwork.ts b/src/cli/commands/joinNetwork.ts new file mode 100644 index 00000000..faa37362 --- /dev/null +++ b/src/cli/commands/joinNetwork.ts @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ArweaveSigner } from '@dha-team/arbundles/node'; +import prompts from 'prompts'; + +import { WalletOptions } from '../options.js'; +import { + jwkToAddress, + requiredJwkFromOptions, + writeIOFromOptions, +} from '../utils.js'; + +export type JoinNetworkOptions = WalletOptions & { + quantity: number | undefined; + disableAutoStake: boolean; + disableDelegatedStaking: boolean; + minDelegatedStake: number | undefined; + delegateRewardShareRatio: number | undefined; + label: string | undefined; + note: string | undefined; + properties: string | undefined; + observer: string | undefined; + fqdn: string | undefined; + port: number | undefined; + protocol: string | undefined; + allowedDelegates: string[] | undefined; +}; + +export async function joinNetwork(options: JoinNetworkOptions) { + const jwk = requiredJwkFromOptions(options); + const io = writeIOFromOptions(options, new ArweaveSigner(jwk)); + + const { + disableDelegatedStaking, + disableAutoStake, + delegateRewardShareRatio, + fqdn, + label, + minDelegatedStake, + note, + observer, + port, + properties, + quantity, + allowedDelegates, + } = options; + const minOperatorStake = 50_000_000_000; // 50,000 IO + const operatorStake = quantity ?? minOperatorStake; + + if (label === undefined) { + throw new Error( + 'Label is required. Please provide a --label for your node.', + ); + } + if (fqdn === undefined) { + throw new Error('FQDN is required. Please provide a --fqdn for your node.'); + } + + const address = jwkToAddress(jwk); + if (!options.skipConfirmation) { + const balance = await io.getBalance({ address }); + + if (balance < operatorStake) { + throw new Error( + `Insufficient balance. Required: ${operatorStake} mIO, available: ${balance} mIO`, + ); + } + + const { confirm } = await prompts({ + type: 'confirm', + name: 'confirm', + message: `You are about to stake ${operatorStake} IO to join the AR.IO network. Are you sure?`, + initial: true, + }); + + if (!confirm) { + console.log('Aborted join-network command by user'); + return; + } + } + + const result = await io.joinNetwork({ + observerAddress: observer, + operatorStake, + allowDelegatedStaking: + disableDelegatedStaking === undefined + ? undefined + : !disableDelegatedStaking, + autoStake: disableAutoStake === undefined ? undefined : !disableAutoStake, + delegateRewardShareRatio, + allowedDelegates, + fqdn, + label, + minDelegatedStake, + note, + port, + properties, + }); + + const output = { + joinNetworkResult: result, + address, + message: `Congratulations! You have successfully joined the AR.IO network (; `, + }; + + console.log(output); +} diff --git a/src/cli/options.ts b/src/cli/options.ts index 0804b4f4..a6691c73 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -28,29 +28,92 @@ export const optionMap = { description: 'Private key to use with the action', }, dev: { - alias: '-d, --dev', - description: 'Run against the dev-net process', - default: false, + alias: '--dev', + description: 'Run against the AR.IO devnet process', + type: 'boolean', }, processId: { alias: '--process-id ', description: 'The process ID to interact with', }, - retries: { - alias: '-r, --retries ', - description: 'The number of times to retry the interaction', - }, - tags: { - alias: '-t, --tags ', - description: 'The tags to use for the interaction', + debug: { + alias: '--debug', + description: 'Enable debug log output', + type: 'boolean', }, address: { alias: '-a, --address
', description: 'The address to interact with', }, + quantity: { + alias: '-q, --quantity ', + description: 'The quantity of mIO to interact with', + }, + disableAutoStake: { + alias: '--disable-auto-stake', + description: 'Disallow auto-staking of operator rewards', + type: 'boolean', + }, + disableDelegatedStaking: { + alias: '--disable-delegated-staking', + description: 'Disallow delegating stake to the gateway', + type: 'boolean', + }, + minDelegatedStake: { + alias: '--min-delegated-stake ', + description: 'The minimum delegated stake allowed', + }, + delegateRewardShareRatio: { + alias: '--delegate-reward-share-ratio ', + description: 'The percentage of rewards to share with delegates', + }, + label: { + alias: '--label