Skip to content

Commit

Permalink
Merge pull request #315 from entropyxyz/naynay/any-balance
Browse files Browse the repository at this point in the history
[NayNay] Balance for any address & transfer (using only substrate api)
  • Loading branch information
frankiebee authored Jan 8, 2025
2 parents 085620f + b5089be commit 010f672
Show file tree
Hide file tree
Showing 23 changed files with 201 additions and 127 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil
- Shared
- updated return data displayed to user on account creation (create or import) [#311](https://github.com/entropyxyz/cli/pull/311)
- Balance now displays the number of BITS to the nearest 4 decimal places [#306](https://github.com/entropyxyz/cli/pull/306)
- removed use of entropy instance from transfer flow [#329](https://github.com/entropyxyz/cli/pull/329)

- CLI
- updated balance command to take in any address, and be able to return the balance for the inputted address [#315](https://github.com/entropyxyz/cli/pull/315)

- TUI
- updated regsitration and transfer flow to use progress loader to provide a signal to the user something is happening [#324](https://github.com/entropyxyz/cli/pull/324)
- removed use of progress bar throughout TUI [#324](https://github.com/entropyxyz/cli/pull/324)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"homepage": "https://github.com/entropyxyz/cli#readme",
"dependencies": {
"@entropyxyz/sdk": "0.4.0",
"@entropyxyz/sdk": "0.4.1-0",
"ajv": "^8.17.1",
"commander": "^12.1.0",
"env-paths": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/account/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function selectAndPersistNewAccount (configPath: string, newAccount
})
}

export async function persistVerifyingKeyToAccount (configPath: string, verifyingKey: string, accountNameOrAddress: string) {
export async function persistVerifyingKeyToAccount (configPath: string, verifyingKey: string, accountNameOrAddress?: string) {
const storedConfig = await config.get(configPath)
const { accounts } = storedConfig

Expand Down
47 changes: 23 additions & 24 deletions src/balance/command.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,37 @@
import { Command } from "commander";
import Entropy from "@entropyxyz/sdk";

// @ts-expect-error
import { isValidSubstrateAddress } from '@entropyxyz/sdk/utils'
import { EntropyBalance } from "./main";
import { BalanceInfo } from "./types";
import { configOption, endpointOption, cliWrite } from "../common/utils-cli";
import { findAccountByAddressOrName, getTokenDetails, lilBitsToBits, round } from "../common/utils";
import { loadEntropyCli } from "../common/load-entropy"
import * as config from "../config";
import { EntropyConfigAccount } from "src/config/types";
import { closeSubstrate, getLoadedSubstrate } from "src/common/substrate-utils";

export function entropyBalanceCommand () {
const balanceCommand = new Command('balance')
// account can no longer be a required argument if a user wishes to
// view the balances of all accounts
balanceCommand
.description('Command to retrieive the balance of an account on the Entropy Network')
.argument('[account] <address|name>', [
.argument('[account]', [
'The address an account address whose balance you want to query.',
'Can also be the human-readable name of one of your accounts'
'Can also be the human-readable name of one of your accounts.'
].join(' '))
.option('-a, --all', 'Get balances for all admin accounts in the config')
.addOption(configOption())
.addOption(endpointOption())
.action(async (account, opts) => {
const substrate = await getLoadedSubstrate(opts.endpoint)
const BalanceService = new EntropyBalance(substrate, opts.endpoint)
const { decimals, symbol } = await getTokenDetails(substrate)
const toBits = (lilBits: number) => round(lilBitsToBits(lilBits, decimals))
const { accounts } = await config.get(opts.config)

let entropy: Entropy
if (!account && opts.all) {
const tempAddress = accounts[0].address
entropy = await loadEntropyCli({ account: tempAddress, ...opts })
} else if (account && !opts.all) {
entropy = await loadEntropyCli({ account, ...opts })
} else {
return balanceCommand.help()
}

const balanceService = new EntropyBalance(entropy, opts.endpoint)
const { decimals, symbol } = await getTokenDetails(entropy)
const toBits = (nanoBits: number) => round(lilBitsToBits(nanoBits, decimals))

if (opts.all) {
// Balances for all admin accounts
const addresses: string[] = accounts.map((acct: EntropyConfigAccount) => acct.address)
const balances = await balanceService.getBalances(addresses)
const balances = await BalanceService.getBalances(addresses)
.then((infos: BalanceInfo[]) => {
return infos.map(info => {
return {
Expand All @@ -56,13 +45,23 @@ export function entropyBalanceCommand () {
})
cliWrite(balances)
} else {
let address = findAccountByAddressOrName(accounts, account)?.address
if (!address) {
// provided account does not exist in the users config
if (isValidSubstrateAddress(account)) address = account
else {
// account is either null or not a valid substrate address
console.error(`Provided [account=${account}] is not a valid substrate address`)
process.exit(1)
}
}
// Balance for singular account
const address = findAccountByAddressOrName(accounts, account)?.address
const balance = await balanceService.getBalance(address)
const balance = await BalanceService.getAnyBalance(address)
.then(toBits)
cliWrite({ account, balance, symbol })
}

// closing substrate
await closeSubstrate(substrate)
process.exit(0)
})

Expand Down
18 changes: 11 additions & 7 deletions src/balance/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { EntropyBalance } from "./main"
import { findAccountByAddressOrName, getTokenDetails, print, round, lilBitsToBits } from "src/common/utils"

import { closeSubstrate, getLoadedSubstrate } from '../common/substrate-utils'
import { findAccountByAddressOrName, getTokenDetails, print, round, lilBitsToBits } from "../common/utils"
import { EntropyTuiOptions } from '../types'

export async function entropyBalance (entropy, opts: EntropyTuiOptions, storedConfig) {
import { EntropyBalance } from "./main"

export async function entropyBalance (opts: EntropyTuiOptions, storedConfig) {
try {
const substrate = await getLoadedSubstrate(opts.endpoint)
const BalanceService = new EntropyBalance(substrate, opts.endpoint)
// grabbing decimals from chain spec as that is the source of truth for the value
const { decimals, symbol } = await getTokenDetails(entropy)
const balanceService = new EntropyBalance(entropy, opts.endpoint)
const { decimals, symbol } = await getTokenDetails(substrate)
const address = findAccountByAddressOrName(storedConfig.accounts, storedConfig.selectedAccount)?.address
const lilBalance = await balanceService.getBalance(address)
const lilBalance = await BalanceService.getAnyBalance(address)
const balance = round(lilBitsToBits(lilBalance, decimals))
print(`Entropy Account [${storedConfig.selectedAccount}] (${address}) has a balance of: ${balance} ${symbol}`)
// closing substrate
await closeSubstrate(substrate)
} catch (error) {
console.error('There was an error retrieving balance', error)
}
Expand Down
16 changes: 7 additions & 9 deletions src/balance/main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import Entropy from "@entropyxyz/sdk"
import { EntropyBase } from "../common/entropy-base"
import * as BalanceUtils from "./utils"
import { BalanceInfo } from "./types"
import { EntropySubstrateBase } from "src/common/entropy-substrate-base"

const FLOW_CONTEXT = 'ENTROPY-BALANCE'
export class EntropyBalance extends EntropyBase {
constructor (entropy: Entropy, endpoint: string) {
super({ entropy, endpoint, flowContext: FLOW_CONTEXT })
export class EntropyBalance extends EntropySubstrateBase {
constructor (substrate: any, endpoint: string) {
super({ substrate, endpoint, flowContext: FLOW_CONTEXT })
}

async getBalance (address: string): Promise<number> {
const accountInfo = (await this.entropy.substrate.query.system.account(address)) as any
async getAnyBalance (address: string) {
const accountInfo = (await this.substrate.query.system.account(address)) as any
const balance = parseInt(BalanceUtils.hexToBigInt(accountInfo.data.free).toString())

this.logger.log(`Current balance of ${address}: ${balance}`, EntropyBalance.name)
return balance
}

async getBalances (addresses: string[]): Promise<BalanceInfo[]> {
return Promise.all(
addresses.map(async address => {
return this.getBalance(address)
return this.getAnyBalance(address)
.then((balance: number) => {
return { address, balance }
})
Expand Down
13 changes: 13 additions & 0 deletions src/common/entropy-substrate-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EntropyLogger } from "./logger";

export abstract class EntropySubstrateBase {
protected logger: EntropyLogger
protected substrate: any
protected endpoint: string

constructor ({ substrate, endpoint, flowContext }: { substrate: any, endpoint: string, flowContext: string }) {
this.logger = new EntropyLogger(flowContext, endpoint)
this.substrate = substrate
this.endpoint = endpoint
}
}
4 changes: 2 additions & 2 deletions src/common/load-entropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ async function loadEntropy (opts: LoadEntropyOpts): Promise<Entropy> {
const storedConfig = await opts.config.get()
if (!storedConfig) throw Error('no config!!') // TEMP: want to see if we hit this!

let account = resolveAccount(storedConfig, opts.account)
let account = resolveAccount(storedConfig, opts.account || storedConfig.selectedAccount)
const endpoint = resolveEndpoint(storedConfig, opts.endpoint)
// NOTE: while it would be nice to parse opts --account, --endpoint with Commander
// the argParser for these Options does not have access to the --config option,
Expand Down Expand Up @@ -211,7 +211,7 @@ async function setupRegistrationSubAccount (account: EntropyConfigAccount, confi
}

const keyringCache = {}
async function loadKeyring (account: EntropyConfigAccount) {
export async function loadKeyring (account: EntropyConfigAccount) {
const { address } = account.data.admin || {}
if (!address) throw new Error('Cannot load keyring, no admin address')

Expand Down
18 changes: 18 additions & 0 deletions src/common/substrate-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @ts-expect-error
import { createSubstrate } from '@entropyxyz/sdk/utils'

export async function getLoadedSubstrate (endpoint: string) {
const substrate = createSubstrate(endpoint)
await substrate.isReadyOrError
return substrate
}

export async function closeSubstrate (substrate: any) {
try {
// closing substrate
await substrate.disconnect()
} catch (error) {
console.error('SubstrateError: Error closing connection', error)
throw error
}
}
9 changes: 4 additions & 5 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Entropy } from '@entropyxyz/sdk'
import { Buffer } from 'node:buffer'
import { homedir } from 'node:os'
import { join } from 'node:path'
Expand Down Expand Up @@ -111,11 +110,11 @@ export function absolutePath (somePath: string) {
}
}

export function formatDispatchError (entropy: Entropy, dispatchError) {
export function formatDispatchError (substrate: any, dispatchError) {
let msg: string
if (dispatchError.isModule) {
// for module errors, we have the section indexed, lookup
const decoded = entropy.substrate.registry.findMetaError(
const decoded = substrate.registry.findMetaError(
dispatchError.asModule
)
const { docs, name, section } = decoded
Expand Down Expand Up @@ -147,9 +146,9 @@ export async function jumpStartNetwork (entropy, endpoint): Promise<any> {

// caching details to reduce number of calls made to the rpc endpoint
let tokenDetails: TokenDetails
export async function getTokenDetails (entropy): Promise<TokenDetails> {
export async function getTokenDetails (substrate): Promise<TokenDetails> {
if (tokenDetails) return tokenDetails
const chainProperties = await entropy.substrate.rpc.system.properties()
const chainProperties = await substrate.rpc.system.properties()
const decimals = chainProperties.tokenDecimals.toHuman()[0]
const symbol = chainProperties.tokenSymbol.toHuman()[0]
tokenDetails = { decimals: parseInt(decimals), symbol }
Expand Down
8 changes: 4 additions & 4 deletions src/faucet/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export async function entropyFaucet (entropy: Entropy, opts: EntropyTuiOptions,
throw new Error("Keys are undefined")
}

const { decimals } = await getTokenDetails(entropy)
const { decimals } = await getTokenDetails(entropy.substrate)
const amount = bitsToLilBits(2, decimals)
const faucetService = new EntropyFaucet(entropy, opts.endpoint)
const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys()
// @ts-expect-error
return sendMoneyFromRandomFaucet(entropy, options.endpoint, verifyingKeys, amount.toString(), logger)
// @ts-expect-error verifyingKeys
return sendMoneyFromRandomFaucet(entropy, opts.endpoint, verifyingKeys, amount.toString(), logger)
}

// Method that takes in the initial list of verifying keys (to avoid multiple calls to the rpc) and recursively retries each faucet until
Expand All @@ -41,7 +41,7 @@ async function sendMoneyFromRandomFaucet (entropy: Entropy, endpoint: string, ve
faucetSpinner.start()
}
const faucetService = new EntropyFaucet(entropy, endpoint)
const { decimals, symbol } = await getTokenDetails(entropy)
const { decimals, symbol } = await getTokenDetails(entropy.substrate)
const selectedAccountAddress = entropy.keyring.accounts.registration.address
let chosenVerifyingKey: string
try {
Expand Down
6 changes: 3 additions & 3 deletions src/faucet/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class EntropyFaucet extends EntropyBase {
// status would still be set, but in the case of error we can shortcut
// to just check it (so an error would indicate InBlock or Finalized)
if (dispatchError) {
const error = formatDispatchError(this.entropy, dispatchError)
const error = formatDispatchError(this.entropy.substrate, dispatchError)
return reject(error)
}
if (status.isFinalized) resolve(status)
Expand Down Expand Up @@ -71,11 +71,11 @@ export class EntropyFaucet extends EntropyBase {
faucetProgramPointer = FAUCET_PROGRAM_POINTER
}: SendMoneyParams
): Promise<any> {
const balanceService = new EntropyBalance(this.entropy, this.endpoint)
const programService = new EntropyProgram(this.entropy, this.endpoint)
const BalanceService = new EntropyBalance(this.entropy.substrate, this.endpoint)

// check balance of faucet address
const balance = await balanceService.getBalance(faucetAddress)
const balance = await BalanceService.getAnyBalance(faucetAddress)
if (balance <= 0) throw new Error('FundsError: Faucet Account does not have funds')

// check verifying key has ONLY the exact program installed
Expand Down
20 changes: 13 additions & 7 deletions src/transfer/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { Command } from "commander"

import { EntropyTransfer } from "./main"
import { accountOption, configOption, endpointOption, cliWrite } from "../common/utils-cli"
import { loadEntropyCli } from "../common/load-entropy"
import { getTokenDetails } from "../common/utils"
import { loadKeyring } from "../common/load-entropy"
import { findAccountByAddressOrName, getTokenDetails } from "../common/utils"
import * as config from "../config";
import { closeSubstrate, getLoadedSubstrate } from "src/common/substrate-utils"

export function entropyTransferCommand () {
const transferCommand = new Command('transfer')
Expand All @@ -16,18 +18,22 @@ export function entropyTransferCommand () {
.addOption(endpointOption())
.action(async (destination, amount, opts) => {
// TODO: destination as <name|address> ?
const entropy = await loadEntropyCli(opts)
const transferService = new EntropyTransfer(entropy, opts.endpoint)
const { symbol } = await getTokenDetails(entropy)
const { accounts, selectedAccount } = await config.get(opts.config)
const substrate = await getLoadedSubstrate(opts.endpoint)
const account = findAccountByAddressOrName(accounts, opts.account || selectedAccount)
const loadedKeyring = await loadKeyring(account)
const transferService = new EntropyTransfer(substrate, opts.endpoint)
const { symbol } = await getTokenDetails(substrate)

await transferService.transfer(destination, amount)
await transferService.transfer(loadedKeyring.accounts.registration.pair, destination, amount)

cliWrite({
source: opts.account,
source: loadedKeyring.accounts.registration.address,
destination,
amount,
symbol
})
await closeSubstrate(substrate)
process.exit(0)
})
return transferCommand
Expand Down
18 changes: 13 additions & 5 deletions src/transfer/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
import inquirer from "inquirer"
import yoctoSpinner from "yocto-spinner"

import { getTokenDetails, print } from "../common/utils"
import { findAccountByAddressOrName, getTokenDetails, print } from "../common/utils"
import { EntropyTransfer } from "./main"
import { transferInputQuestions } from "./utils"

import { EntropyTuiOptions } from '../types'
import { EntropyConfig } from "src/config/types"
import { closeSubstrate, getLoadedSubstrate } from "src/common/substrate-utils"
import { loadKeyring } from "src/common/load-entropy"

const transferSpinner = yoctoSpinner()
const SPINNER_TEXT = 'Transferring funds...'

export async function entropyTransfer (entropy, opts: EntropyTuiOptions) {
export async function entropyTransfer (opts: EntropyTuiOptions, storedConfig: EntropyConfig) {
transferSpinner.text = SPINNER_TEXT
if (transferSpinner.isSpinning) transferSpinner.stop()
try {
const { symbol } = await getTokenDetails(entropy)
const transferService = new EntropyTransfer(entropy, opts.endpoint)
const substrate = await getLoadedSubstrate(opts.endpoint)
const currentAccount = findAccountByAddressOrName(storedConfig.accounts, opts.account || storedConfig.selectedAccount)
const loadedKeyring = await loadKeyring(currentAccount)
const { symbol } = await getTokenDetails(substrate)
const transferService = new EntropyTransfer(substrate, opts.endpoint)
const { amount, recipientAddress } = await inquirer.prompt(transferInputQuestions)
if (!transferSpinner.isSpinning) transferSpinner.start()
await transferService.transfer(recipientAddress, amount)
await transferService.transfer(loadedKeyring.accounts.registration.pair, recipientAddress, amount)
await closeSubstrate(opts.endpoint)
if (transferSpinner.isSpinning) transferSpinner.stop()
print('')
print(`Transaction successful: Sent ${amount} ${symbol} to ${recipientAddress}`)
print('')
print('Press enter to return to main menu')
} catch (error) {
transferSpinner.text = 'Transfer failed...'
await closeSubstrate(opts.endpoint)
if (transferSpinner.isSpinning) transferSpinner.stop()
print.error('TransferError:', error.message);
}
Expand Down
Loading

0 comments on commit 010f672

Please sign in to comment.