From 40ebe065673b881a3e23b275ea29423aa260259f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Tue, 26 Nov 2024 12:06:05 -0600 Subject: [PATCH] feat(ar.io cli): init leave-network, delegate-stake PE-5854 --- src/cli/cli.ts | 25 +++++++-- src/cli/commands/delegateStake.ts | 84 +++++++++++++++++++++++++++++++ src/cli/commands/transfer.ts | 15 ++---- src/cli/options.ts | 2 + src/cli/types.ts | 4 +- src/cli/utils.ts | 16 ++++++ src/types/io.ts | 11 ++-- 7 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 src/cli/commands/delegateStake.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index f661494b..9f39e8f8 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -21,12 +21,14 @@ import { program } from 'commander'; import { intentsUsingYears, isValidIntent, validIntents } from '../types/io.js'; import { mIOToken } from '../types/token.js'; import { version } from '../version.js'; +import { delegateStake } from './commands/delegateStake.js'; import { joinNetwork } from './commands/joinNetwork.js'; import { transfer } from './commands/transfer.js'; import { updateGatewaySettings } from './commands/updateGatewaySettings.js'; import { addressOptions, arNSAuctionPricesOptions, + delegateStakeOptions, epochOptions, getVaultOptions, globalOptions, @@ -38,6 +40,8 @@ import { paginationOptions, tokenCostOptions, transferOptions, + updateGatewaySettingsOptions, + walletOptions, } from './options.js'; import { AddressAndNameOptions, @@ -50,6 +54,7 @@ import { NameOptions, PaginationAddressOptions, PaginationOptions, + WalletOptions, } from './types.js'; import { addressFromOptions, @@ -63,6 +68,7 @@ import { requiredNameFromOptions, requiredVaultIdFromOptions, runCommand, + writeIOFromOptions, } from './utils.js'; makeCommand({ @@ -163,7 +169,7 @@ makeCommand({ }); makeCommand({ - name: 'get-gateway-delegate-allow-list', + name: 'get-allowed-delegates', description: 'Get the allow list of a gateway delegate', options: paginationAddressOptions, action: async (o) => { @@ -548,14 +554,27 @@ makeCommand({ action: joinNetwork, }); +makeCommand({ + name: 'leave-network', + description: 'Leave a gateway from the AR.IO network', + options: walletOptions, + // TODO: Add a confirmation prompt? Could get settings, display, then confirm prompt + action: (options) => writeIOFromOptions(options).leaveNetwork(), +}); + makeCommand({ name: 'update-gateway-settings', description: 'Update AR.IO gateway settings', - options: joinNetworkOptions, + options: updateGatewaySettingsOptions, action: updateGatewaySettings, }); -// delegate-stake +makeCommand({ + name: 'delegate-stake', + description: 'Delegate stake to a gateway', + options: delegateStakeOptions, + action: delegateStake, +}); // increase-operator-stake diff --git a/src/cli/commands/delegateStake.ts b/src/cli/commands/delegateStake.ts new file mode 100644 index 00000000..37709a99 --- /dev/null +++ b/src/cli/commands/delegateStake.ts @@ -0,0 +1,84 @@ +/** + * 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 prompts from 'prompts'; + +import { mIOToken } from '../../types/token.js'; +import { TransferOptions } from '../types.js'; +import { + formatIOWithCommas, + jwkToAddress, + requiredJwkFromOptions, + requiredTargetAndQuantityFromOptions, + writeIOFromOptions, + writeOptionsFromOptions, +} from '../utils.js'; + +export async function delegateStake(options: TransferOptions) { + const jwk = requiredJwkFromOptions(options); + const address = jwkToAddress(jwk); + const io = writeIOFromOptions(options); + + const { target, ioQuantity } = requiredTargetAndQuantityFromOptions(options); + const mIOQuantity = ioQuantity.toMIO(); + + if (!options.skipConfirmation) { + const balance = await io.getBalance({ address }); + + if (balance < mIOQuantity.valueOf()) { + throw new Error( + `Insufficient IO balance for delegating stake. Balance available: ${new mIOToken(balance).toIO()} IO`, + ); + } + + const targetGateway = await io.getGateway({ address: target }); + if (targetGateway === undefined) { + throw new Error(`Gateway not found for address: ${target}`); + } + if (targetGateway.settings.allowDelegatedStaking === false) { + throw new Error(`Gateway does not allow delegated staking: ${target}`); + } + + // TODO: could get allow list and assert doesn't exist or user is on it + + // TODO: could read from contract to get current delegated stake if there is none, get contract minimum delegated stake. Then see if the new stake value will satisfy minimum delegated stake for both the target gateway settings min delegate stake and contract min delegated amounts + + const { confirm } = await prompts({ + type: 'confirm', + name: 'confirm', + message: `Target Gateway:\n${JSON.stringify(targetGateway, null, 2)}\n\nAre you sure you want to delegate ${formatIOWithCommas(ioQuantity)} IO to ${target}?`, + }); + + if (!confirm) { + return { message: 'Delegate stake aborted by user' }; + } + } + + const result = await io.delegateStake( + { + target, + stakeQty: ioQuantity.toMIO(), + }, + writeOptionsFromOptions(options), + ); + + const output = { + senderAddress: address, + transferResult: result, + message: `Successfully delegated ${formatIOWithCommas(ioQuantity)} IO to ${target}`, + }; + + return output; +} diff --git a/src/cli/commands/transfer.ts b/src/cli/commands/transfer.ts index 87a7942e..3a2cc8d9 100644 --- a/src/cli/commands/transfer.ts +++ b/src/cli/commands/transfer.ts @@ -15,12 +15,13 @@ */ import prompts from 'prompts'; -import { IOToken, mIOToken } from '../../types/token.js'; +import { mIOToken } from '../../types/token.js'; import { TransferOptions } from '../types.js'; import { formatIOWithCommas, jwkToAddress, requiredJwkFromOptions, + requiredTargetAndQuantityFromOptions, writeIOFromOptions, writeOptionsFromOptions, } from '../utils.js'; @@ -30,20 +31,12 @@ export async function transfer(options: TransferOptions) { const address = jwkToAddress(jwk); const io = writeIOFromOptions(options); - const { target, quantity } = options; - if (target === undefined) { - throw new Error('Target address is required'); - } - if (quantity === undefined) { - throw new Error('Quantity is required'); - } - - const ioQuantity = new IOToken(+quantity); + const { target, ioQuantity } = requiredTargetAndQuantityFromOptions(options); if (!options.skipConfirmation) { const balance = await io.getBalance({ address }); - if (balance < quantity) { + if (balance < +ioQuantity.toMIO()) { throw new Error( `Insufficient IO balance for transfer. Balance available: ${new mIOToken(balance).toIO()} IO`, ); diff --git a/src/cli/options.ts b/src/cli/options.ts index 4bb39db5..c0839a29 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -242,6 +242,8 @@ export const transferOptions = [ optionMap.target, ]; +export const delegateStakeOptions = transferOptions; + export const updateGatewaySettingsOptions = [ ...writeActionOptions, optionMap.disableAutoStake, diff --git a/src/cli/types.ts b/src/cli/types.ts index 7172fb89..2f49e938 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -96,10 +96,12 @@ export type WriteActionOptions = WalletOptions & { }; export type TransferOptions = WriteActionOptions & { - quantity: number | undefined; + quantity: string | undefined; target: string | undefined; }; +export type DelegateStakeOptions = TransferOptions; + export type JoinNetworkOptions = WriteActionOptions & CLIOptionsFromAoParams< Omit diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 561c860d..b1abb707 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -42,6 +42,7 @@ import { JsonSerializable, NameOptions, PaginationOptions, + TransferOptions, UpdateGatewaySettingsOptions, VaultIdOptions, WalletOptions, @@ -327,3 +328,18 @@ export function gatewaySettingsFromOptions({ properties, }; } + +export function requiredTargetAndQuantityFromOptions( + options: TransferOptions, +): { target: string; ioQuantity: IOToken } { + if (options.target === undefined) { + throw new Error('No target provided. Use --target'); + } + if (options.quantity === undefined) { + throw new Error('No quantity provided. Use --quantity'); + } + return { + target: options.target, + ioQuantity: new IOToken(+options.quantity), + }; +} diff --git a/src/types/io.ts b/src/types/io.ts index 520bbb6d..616f60b2 100644 --- a/src/types/io.ts +++ b/src/types/io.ts @@ -330,12 +330,13 @@ export type AoAddressParams = { address: WalletAddress; }; -export type AoBalanceParams = { - address: WalletAddress; -}; +export type AoBalanceParams = AoAddressParams; -export type AoPaginatedAddressParams = PaginationParams & { - address: WalletAddress; +export type AoPaginatedAddressParams = PaginationParams & AoAddressParams; + +export type AoDelegateStakeParams = { + target: WalletAddress; + stakeQty: number | mIOToken; }; export type AoGetArNSRecordsParams = PaginationParams;