Skip to content

Commit

Permalink
feat(ar-io cli): init join-network command
Browse files Browse the repository at this point in the history
  • Loading branch information
fedellen committed Nov 19, 2024
1 parent 94c630b commit fc9dc07
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 49 deletions.
2 changes: 1 addition & 1 deletion bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const bundle = async () => {
},
}),
],
external: ['commander'],
external: ['commander', 'prompts'],
tsconfig: './tsconfig.web.json',
outfile: './bundles/web.bundle.min.js',
})
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
28 changes: 23 additions & 5 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 <address> or --wallet-file <wallet-file>
makeCommand({
name: 'balance',
description: 'Get the balance of an address',
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions src/cli/commands/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
Expand Down
120 changes: 120 additions & 0 deletions src/cli/commands/joinNetwork.ts
Original file line number Diff line number Diff line change
@@ -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);
}
106 changes: 93 additions & 13 deletions src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <processId>',
description: 'The process ID to interact with',
},
retries: {
alias: '-r, --retries <retries>',
description: 'The number of times to retry the interaction',
},
tags: {
alias: '-t, --tags <tags>',
description: 'The tags to use for the interaction',
debug: {
alias: '--debug',
description: 'Enable debug log output',
type: 'boolean',
},
address: {
alias: '-a, --address <address>',
description: 'The address to interact with',
},
quantity: {
alias: '-q, --quantity <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 <minDelegatedStake>',
description: 'The minimum delegated stake allowed',
},
delegateRewardShareRatio: {
alias: '--delegate-reward-share-ratio <delegateRewardShareRatio>',
description: 'The percentage of rewards to share with delegates',
},
label: {
alias: '--label <label>',
description: 'The label for the gateway',
},
note: {
alias: '--note <note>',
description: 'The note for the gateway',
},
properties: {
alias: '--properties <properties>',
description: 'The properties for the gateway',
},
observer: {
alias: '--observer <observer>',
description: 'The observer wallet address for the gateway',
},
fqdn: {
alias: '--fqdn <fqdn>',
description: 'The fully qualified domain name for the gateway',
},
port: {
alias: '--port <port>',
description: 'The port for the gateway',
},
protocol: {
alias: '--protocol <protocol>',
description: 'The protocol for the gateway',
},
allowedDelegates: {
alias: '--allowed-delegates <allowedDelegates...>',
description:
'The allowed delegates for the gateway. By default this is empty, meaning all are allowed delegate stake',
type: 'array',
},
skipConfirmation: {
alias: '--skip-confirmation',
description: 'Skip confirmation prompts',
type: 'boolean',
},
};

export const globalOptions = [optionMap.dev, optionMap.processId];
export const globalOptions = [
optionMap.dev,
optionMap.debug,
optionMap.processId,
optionMap.skipConfirmation,
];

export const walletOptions = [
...globalOptions,
Expand All @@ -61,16 +124,33 @@ export const walletOptions = [

export const balanceOptions = [...walletOptions, optionMap.address];

export const joinNetworkOptions = [
...walletOptions,
optionMap.quantity,
optionMap.disableAutoStake,
optionMap.disableDelegatedStaking,
optionMap.minDelegatedStake,
optionMap.delegateRewardShareRatio,
optionMap.label,
optionMap.note,
optionMap.properties,
optionMap.observer,
optionMap.fqdn,
optionMap.port,
optionMap.protocol,
];

// Option Types
export type GlobalOptions = {
dev: boolean;
debug: boolean;
processId: string | undefined;
gateway: string | undefined;
skipConfirmation: boolean;
};

export type WalletOptions = GlobalOptions & {
walletFile: string | undefined;
mnemonic: string | undefined;
// mnemonic: string | undefined;
privateKey: string | undefined;
};

Expand Down
Loading

0 comments on commit fc9dc07

Please sign in to comment.