Skip to content

Commit

Permalink
Merge pull request #3493 from iron-fish/staging
Browse files Browse the repository at this point in the history
STAGING -> MASTER (02/21/23)
  • Loading branch information
NullSoldier authored Feb 21, 2023
2 parents bba0453 + e1b4439 commit cbd402c
Show file tree
Hide file tree
Showing 155 changed files with 5,447 additions and 2,805 deletions.
68 changes: 68 additions & 0 deletions ironfish-cli/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CLI UX Guide

The Iron Fish CLI is for humans before machines. The primary goal of anyone developing CLI plugins should always be usability. Input and output should be consistent across commands to allow the user to easily learn how to interact with new commands.

Based on https://devcenter.heroku.com/articles/cli-style-guide

## Naming the command
The CLI is made up of topics and commands. For the command `ironfish migrations:start`, migrations is the topic and start is the command.

Generally topics are plural nouns and commands are verbs.

Topic and command names should always be a single, lowercase word without spaces, hyphens, underscores, or other word delimiters. Colons, however, are allowed as this is how to define subcommands (such as `ironfish wallet:transactions:add`). If there is no obvious way to avoid having multiple words, separate with kebab-case: `ironfish service:estimate-fee-rates`

Because topics are generally nouns, the root command of a topic usually lists those nouns. So in the case of `ironfish migrations`, it will list all the migrations. Never create a *:list command such as `ironfish migrations:list`.

## Command Description
Topic and command descriptions should be provided for all topics and commands. They should fit on 80 character width screens, begin with a lowercase character, and should not end in a period.

## Input
Input to commands is typically provided by flags and args. Stdin can also be used in cases where it is useful to stream files or information in (such as with heroku run).

### Flags

Flags are preferred to args when there are many inputs, particularly inputs of the same type. They involve a bit more typing, but make the use of the CLI clearer. For example, `ironfish wallet:send` used to accept an argument for the account to use, as well as --to flag to specify the account to send to.

So using `ironfish wallet:send` used to work like this:

```bash
$ ironfish wallet:send source_account --to dest_account
```

This is confusing to the user since it isn’t clear which account they are sending from and which one they are sending to. By switching to required flags, we instead expect input in this form:

```bash
ironfish wallet:send --from source_account --to dest_account
```

This also allows the user to specify the flags in any order, and gives them the confidence that they are running the command correctly. It also allows us to show better error messages.

Ensure that descriptions are provided for all flags, that the descriptions are in lowercase, that they are concise (so as to fit on narrow screens), and that they do not end in a period to match other flag descriptions.


### Arguments
Arguments are the basic way to provide input for a command. While flags are generally preferred, they are sometimes unnecessary in cases where there is only 1 argument, or the arguments are obvious and in an obvious order.

In the case of `ironfish chain:power`, we can specify the block sequence we want to determine the network mining power with an argument. We can also specify how many blocks back to look to average the mining power with a flag.

```bash
ironfish chain:power 5432 --history 20
```

If this was done with only arguments, it wouldn’t be clear if the sequence should go before or after the number of blocks to use to sample the network mining power. Using a required flag instead allows the user to specify it either way.

### Prompting
Prompting for missing input provides a nice way to show complicated options in the CLI. For example, `ironfish wallet:use` shows the following if no account is specified as an arg.

```
$ ironfish wallet:use
? Which wallet would you like to use? (Use arrow keys)
❯ vitalik
satoshi
jason
```

> ℹ️ Use [inquirer](https://github.com/sboudrias/Inquirer.js) to show prompts like this.
However, if prompting is required to complete a command, this means the user will not be able to script the command. Ensure that args or flags can always be provided to bypass the prompt. In this case, `ironfish wallet:use` can take in an argument for the account to set as default to skip the prompt.
7 changes: 4 additions & 3 deletions ironfish-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ironfish",
"version": "0.1.67",
"version": "0.1.68",
"description": "CLI for running and interacting with an Iron Fish node",
"author": "Iron Fish <[email protected]> (https://ironfish.network)",
"main": "build/src/index.js",
Expand Down Expand Up @@ -59,8 +59,8 @@
"@aws-sdk/s3-request-presigner": "3.127.0",
"@aws-sdk/client-cognito-identity": "3.215.0",
"@aws-sdk/client-s3": "3.127.0",
"@ironfish/rust-nodejs": "0.1.26",
"@ironfish/sdk": "0.0.44",
"@ironfish/rust-nodejs": "0.1.27",
"@ironfish/sdk": "0.0.45",
"@oclif/core": "1.23.1",
"@oclif/plugin-help": "5.1.12",
"@oclif/plugin-not-found": "2.3.1",
Expand All @@ -70,6 +70,7 @@
"blessed": "0.1.81",
"blru": "0.1.6",
"buffer-map": "0.0.7",
"chalk": "4.1.2",
"inquirer": "8.2.5",
"json-colorizer": "2.2.2",
"segfault-handler": "1.3.0",
Expand Down
2 changes: 1 addition & 1 deletion ironfish-cli/src/commands/blocks/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class ShowBlock extends IronfishCommand {
const search = args.search as string

const client = await this.sdk.connectRpc()
const data = await client.getBlockInfo({ search })
const data = await client.getBlock({ search })

this.log(JSON.stringify(data.content, undefined, ' '))
}
Expand Down
37 changes: 17 additions & 20 deletions ironfish-cli/src/commands/chain/power.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,47 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { FileUtils } from '@ironfish/sdk'
import { Flags } from '@oclif/core'
import { parseNumber } from '../../args'
import { IronfishCommand } from '../../command'
import { LocalFlags } from '../../flags'

export default class Power extends IronfishCommand {
static description = "Show the network's hash power (hash/s)"
static description = "Show the network's hash power per second"

static flags = {
...LocalFlags,
history: Flags.integer({
required: false,
description:
'The number of blocks to look back to calculate the network hashes per second',
}),
}

static args = [
{
name: 'blocks',
parse: (input: string): Promise<number | null> => Promise.resolve(parseNumber(input)),
required: false,
description:
'The number of blocks to look back to calculate the power. This value must be > 0',
},
{
name: 'sequence',
name: 'block',
parse: (input: string): Promise<number | null> => Promise.resolve(parseNumber(input)),
required: false,
description: 'The sequence of the latest block from when to estimate network speed ',
description: 'The sequence of the block to estimate network speed for',
},
]

async start(): Promise<void> {
const { args } = await this.parse(Power)
const inputBlocks = args.blocks as number | null | undefined
const inputSequence = args.sequence as number | null | undefined
const { flags, args } = await this.parse(Power)
const block = args.block as number | null | undefined

await this.sdk.client.connect()
const client = await this.sdk.connectRpc()

const data = await this.sdk.client.getNetworkHashPower({
blocks: inputBlocks,
sequence: inputSequence,
const data = await client.getNetworkHashPower({
sequence: block,
blocks: flags.history,
})

const { hashesPerSecond, blocks, sequence } = data.content
const formattedHashesPerSecond = FileUtils.formatHashRate(hashesPerSecond)
const formattedHashesPerSecond = FileUtils.formatHashRate(data.content.hashesPerSecond)

this.log(
`The network power for block ${sequence} was ${formattedHashesPerSecond} averaged over ${blocks} previous blocks.`,
`The network power for block ${data.content.sequence} was ${formattedHashesPerSecond} averaged over ${data.content.blocks} previous blocks.`,
)
}
}
18 changes: 15 additions & 3 deletions ironfish-cli/src/commands/config/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export class GetCommand extends IronfishCommand {
allowNo: true,
description: 'Should colorize the output',
}),
json: Flags.boolean({
default: false,
allowNo: true,
description: 'Output the config value as json',
}),
}

async start(): Promise<void> {
Expand All @@ -51,9 +56,16 @@ export class GetCommand extends IronfishCommand {
this.exit(0)
}

let output = JSON.stringify(response.content[key], undefined, ' ')
if (flags.color) {
output = jsonColorizer(output)
let output = ''

if (flags.json) {
output = JSON.stringify(response.content[key], undefined, ' ')

if (flags.color) {
output = jsonColorizer(output)
}
} else {
output = String(response.content[key])
}

this.log(output)
Expand Down
39 changes: 39 additions & 0 deletions ironfish-cli/src/commands/config/unset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Flags } from '@oclif/core'
import { IronfishCommand } from '../../command'
import { RemoteFlags } from '../../flags'

export class UnsetCommand extends IronfishCommand {
static description = `Unset a value in the config and fall back to default`

static args = [
{
name: 'name',
parse: (input: string): Promise<string> => Promise.resolve(input.trim()),
required: true,
description: 'Name of the config item',
},
]

static flags = {
...RemoteFlags,
local: Flags.boolean({
default: false,
description: 'Dont connect to the node when updating the config',
}),
}

static examples = ['$ ironfish config:unset blockGraffiti']

async start(): Promise<void> {
const { args, flags } = await this.parse(UnsetCommand)
const name = args.name as string

const client = await this.sdk.connectRpc(flags.local)
await client.unsetConfig({ name })

this.exit(0)
}
}
2 changes: 1 addition & 1 deletion ironfish-cli/src/commands/faucet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class FaucetCommand extends IronfishCommand {

try {
await client.getFunds({
accountName,
account: accountName,
email,
})
} catch (error: unknown) {
Expand Down
2 changes: 1 addition & 1 deletion ironfish-cli/src/commands/service/faucet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export default class Faucet extends IronfishCommand {
})

const tx = await client.sendTransaction({
fromAccountName: account,
account,
outputs,
fee: BigInt(faucetTransactions.length * FAUCET_FEE).toString(),
})
Expand Down
4 changes: 2 additions & 2 deletions ironfish-cli/src/commands/service/sync-multi-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export default class SyncMultiAsset extends IronfishCommand {

let lastCountedSequence: number
if (head) {
const blockInfo = await client.getBlockInfo({ hash: head })
lastCountedSequence = blockInfo.content.block.sequence
const block = await client.getBlock({ hash: head })
lastCountedSequence = block.content.block.sequence
} else {
lastCountedSequence = GENESIS_BLOCK_SEQUENCE
}
Expand Down
11 changes: 9 additions & 2 deletions ironfish-cli/src/commands/testnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default class Testnet extends IronfishCommand {
} else {
// Fetch by graffiti
if (!userArg || userArg.length === 0) {
this.log(`Could not figure out testnet user, graffiti was not provided`)
this.log(`ERROR: Could not figure out testnet user, graffiti was not provided`)
return this.exit(1)
}

Expand All @@ -102,7 +102,14 @@ export default class Testnet extends IronfishCommand {
const user = await api.findUser({ graffiti: userArg })

if (!user) {
this.log(`Could not find a user with graffiti ${userArg}`)
this.log(`ERROR: Could not find a user with graffiti ${userArg}`)
return this.exit(1)
}

if (!user.verified) {
this.log(
`ERROR: You must verify your Iron Fish email. Visit https://ironfish.network and click "Log In" to do so.`,
)
return this.exit(1)
}

Expand Down
16 changes: 13 additions & 3 deletions ironfish-cli/src/commands/wallet/burn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { IronfishCommand } from '../../command'
import { IronFlag, parseIron, RemoteFlags } from '../../flags'
import { ProgressBar } from '../../types'
import { selectAsset } from '../../utils/asset'
import { doEligibilityCheck } from '../../utils/testnet'

export class Burn extends IronfishCommand {
static description = 'Burn tokens and decrease supply for a given asset'
Expand Down Expand Up @@ -67,12 +68,21 @@ export class Burn extends IronfishCommand {
description:
'The block sequence that the transaction can not be mined after. Set to 0 for no expiration.',
}),
eligibility: Flags.boolean({
default: true,
allowNo: true,
description: 'check testnet eligibility',
}),
}

async start(): Promise<void> {
const { flags } = await this.parse(Burn)
const client = await this.sdk.connectRpc(false, true)

if (flags.eligibility) {
await doEligibilityCheck(client, this.logger)
}

const status = await client.getNodeStatus()
if (!status.content.blockchain.synced) {
this.log(
Expand Down Expand Up @@ -133,7 +143,7 @@ export class Burn extends IronfishCommand {
if (flags.fee) {
fee = CurrencyUtils.encode(flags.fee)
const createResponse = await client.createTransaction({
sender: account,
account,
outputs: [],
burns: [
{
Expand All @@ -159,7 +169,7 @@ export class Burn extends IronfishCommand {
const feeRateOptions: { value: number; name: string }[] = []

const createTransactionRequest: CreateTransactionRequest = {
sender: account,
account,
outputs: [],
burns: [
{
Expand Down Expand Up @@ -265,7 +275,7 @@ ${CurrencyUtils.renderIron(
try {
const result = await client.postTransaction({
transaction: rawTransactionResponse,
sender: account,
account,
})

stopProgressBar()
Expand Down
14 changes: 7 additions & 7 deletions ironfish-cli/src/commands/wallet/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,23 @@ export class DeleteCommand extends IronfishCommand {
async start(): Promise<void> {
const { args, flags } = await this.parse(DeleteCommand)
const confirm = flags.confirm
const name = args.account as string
const account = args.account as string

const client = await this.sdk.connectRpc()

const response = await client.removeAccount({ name, confirm })
const response = await client.removeAccount({ account, confirm })

if (response.content.needsConfirm) {
const value = await CliUx.ux.prompt(`Are you sure? Type ${name} to confirm`)
const value = await CliUx.ux.prompt(`Are you sure? Type ${account} to confirm`)

if (value !== name) {
this.log(`Aborting: ${value} did not match ${name}`)
if (value !== account) {
this.log(`Aborting: ${value} did not match ${account}`)
this.exit(1)
}

await client.removeAccount({ name, confirm: true })
await client.removeAccount({ account, confirm: true })
}

this.log(`Account '${name}' successfully deleted.`)
this.log(`Account '${account}' successfully deleted.`)
}
}
Loading

0 comments on commit cbd402c

Please sign in to comment.