|
@@ -1,6 +1,5 @@
|
|
|
import fs from 'fs'
|
|
|
import path from 'path'
|
|
|
-import slug from 'slug'
|
|
|
import inquirer from 'inquirer'
|
|
|
import ExitCodes from '../ExitCodes'
|
|
|
import { CLIError } from '@oclif/errors'
|
|
@@ -10,9 +9,22 @@ import { formatBalance } from '@polkadot/util'
|
|
|
import { NamedKeyringPair } from '../Types'
|
|
|
import { DeriveBalancesAll } from '@polkadot/api-derive/types'
|
|
|
import { toFixedLength } from '../helpers/display'
|
|
|
+import { AccountId, MemberId } from '@joystream/types/common'
|
|
|
+import { KeyringPair, KeyringInstance, KeyringOptions } from '@polkadot/keyring/types'
|
|
|
+import { KeypairType } from '@polkadot/util-crypto/types'
|
|
|
+import createDevelopmentKeyring from '@polkadot/keyring/testing'
|
|
|
+import chalk from 'chalk'
|
|
|
+import { mnemonicGenerate } from '@polkadot/util-crypto'
|
|
|
+import { validateAddress } from '../helpers/validation'
|
|
|
+import slug from 'slug'
|
|
|
+import { Membership } from '@joystream/types/members'
|
|
|
+import BN from 'bn.js'
|
|
|
|
|
|
const ACCOUNTS_DIRNAME = 'accounts'
|
|
|
-const SPECIAL_ACCOUNT_POSTFIX = '__DEV'
|
|
|
+export const DEFAULT_ACCOUNT_TYPE = 'sr25519'
|
|
|
+export const KEYRING_OPTIONS: KeyringOptions = {
|
|
|
+ type: DEFAULT_ACCOUNT_TYPE,
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
* Abstract base class for account-related commands.
|
|
@@ -22,16 +34,37 @@ const SPECIAL_ACCOUNT_POSTFIX = '__DEV'
|
|
|
* Where: APP_DATA_PATH is provided by StateAwareCommandBase and ACCOUNTS_DIRNAME is a const (see above).
|
|
|
*/
|
|
|
export default abstract class AccountsCommandBase extends ApiCommandBase {
|
|
|
+ private keyring: KeyringInstance | undefined
|
|
|
+
|
|
|
+ getKeyring(): KeyringInstance {
|
|
|
+ if (!this.keyring) {
|
|
|
+ this.error('Trying to access Keyring before AccountsCommandBase initialization', {
|
|
|
+ exit: ExitCodes.UnexpectedException,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return this.keyring
|
|
|
+ }
|
|
|
+
|
|
|
+ isKeyAvailable(key: AccountId | string): boolean {
|
|
|
+ return this.getKeyring()
|
|
|
+ .getPairs()
|
|
|
+ .some((p) => p.address === key.toString())
|
|
|
+ }
|
|
|
+
|
|
|
getAccountsDirPath(): string {
|
|
|
return path.join(this.getAppDataPath(), ACCOUNTS_DIRNAME)
|
|
|
}
|
|
|
|
|
|
- getAccountFilePath(account: NamedKeyringPair, isSpecial = false): string {
|
|
|
- return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account, isSpecial))
|
|
|
+ getAccountFileName(accountName: string): string {
|
|
|
+ return `${slug(accountName)}.json`
|
|
|
+ }
|
|
|
+
|
|
|
+ getAccountFilePath(accountName: string): string {
|
|
|
+ return path.join(this.getAccountsDirPath(), this.getAccountFileName(accountName))
|
|
|
}
|
|
|
|
|
|
- generateAccountFilename(account: NamedKeyringPair, isSpecial = false): string {
|
|
|
- return `${slug(account.meta.name, '_')}__${account.address}${isSpecial ? SPECIAL_ACCOUNT_POSTFIX : ''}.json`
|
|
|
+ isAccountNameTaken(accountName: string): boolean {
|
|
|
+ return this.getPairs().some((p) => this.getAccountFileName(p.meta.name) === this.getAccountFileName(accountName))
|
|
|
}
|
|
|
|
|
|
private initAccountsFs(): void {
|
|
@@ -40,23 +73,59 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- saveAccount(account: NamedKeyringPair, password: string, isSpecial = false): void {
|
|
|
- try {
|
|
|
- const destPath = this.getAccountFilePath(account, isSpecial)
|
|
|
- fs.writeFileSync(destPath, JSON.stringify(account.toJson(password)))
|
|
|
- } catch (e) {
|
|
|
- throw this.createDataWriteError()
|
|
|
+ async createAccount(
|
|
|
+ name?: string,
|
|
|
+ masterKey?: KeyringPair,
|
|
|
+ password?: string,
|
|
|
+ type?: KeypairType
|
|
|
+ ): Promise<NamedKeyringPair> {
|
|
|
+ while (!name || this.isAccountNameTaken(name)) {
|
|
|
+ if (name) {
|
|
|
+ this.warn(`Account ${chalk.white(name)} already exists... Try different name`)
|
|
|
+ }
|
|
|
+ name = await this.simplePrompt({ message: 'New account name' })
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // Add dev "Alice" and "Bob" accounts
|
|
|
- initSpecialAccounts() {
|
|
|
- const keyring = new Keyring({ type: 'sr25519' })
|
|
|
- keyring.addFromUri('//Alice', { name: 'Alice' })
|
|
|
- keyring.addFromUri('//Bob', { name: 'Bob' })
|
|
|
- keyring
|
|
|
- .getPairs()
|
|
|
- .forEach((pair) => this.saveAccount({ ...pair, meta: { name: pair.meta.name as string } }, '', true))
|
|
|
+ if (!masterKey) {
|
|
|
+ const keyring = new Keyring(KEYRING_OPTIONS)
|
|
|
+ const mnemonic = mnemonicGenerate()
|
|
|
+ keyring.addFromMnemonic(mnemonic, { name, whenCreated: Date.now() }, type)
|
|
|
+ masterKey = keyring.getPairs()[0]
|
|
|
+ this.log(chalk.white(`${chalk.bold('New account memonic: ')}${mnemonic}`))
|
|
|
+ } else {
|
|
|
+ const existingAcc = this.getPairs().find((p) => p.address === masterKey!.address)
|
|
|
+ if (existingAcc) {
|
|
|
+ this.error(`Account with this key already exists (${chalk.white(existingAcc.meta.name)})`, {
|
|
|
+ exit: ExitCodes.InvalidInput,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ await this.requestPairDecoding(masterKey, 'Current account password')
|
|
|
+ if (!masterKey.meta.name) {
|
|
|
+ masterKey.meta.name = name
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ while (password === undefined) {
|
|
|
+ password = await this.promptForPassword("Set new account's password")
|
|
|
+ const password2 = await this.promptForPassword("Confirm new account's password")
|
|
|
+
|
|
|
+ if (password !== password2) {
|
|
|
+ this.warn('Passwords are not the same!')
|
|
|
+ password = undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!password) {
|
|
|
+ this.warn('Using empty password is not recommended!')
|
|
|
+ }
|
|
|
+
|
|
|
+ const destPath = this.getAccountFilePath(name)
|
|
|
+ fs.writeFileSync(destPath, JSON.stringify(masterKey.toJson(password)))
|
|
|
+
|
|
|
+ this.getKeyring().addPair(masterKey)
|
|
|
+
|
|
|
+ this.log(chalk.greenBright(`\nNew account succesfully created!`))
|
|
|
+
|
|
|
+ return masterKey as NamedKeyringPair
|
|
|
}
|
|
|
|
|
|
fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
|
|
@@ -76,18 +145,20 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
|
|
|
throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile })
|
|
|
}
|
|
|
|
|
|
- // Force some default account name if none is provided in the original backup
|
|
|
if (!accountJsonObj.meta) accountJsonObj.meta = {}
|
|
|
- if (!accountJsonObj.meta.name) accountJsonObj.meta.name = 'Unnamed Account'
|
|
|
+ // Normalize the CLI account name based on file name
|
|
|
+ // (makes sure getFilePath(name) will always point to the correct file, preserving backward-compatibility
|
|
|
+ // with older CLI versions)
|
|
|
+ accountJsonObj.meta.name = path.basename(jsonBackupFilePath, '.json')
|
|
|
|
|
|
- const keyring = new Keyring()
|
|
|
+ const keyring = new Keyring(KEYRING_OPTIONS)
|
|
|
let account: NamedKeyringPair
|
|
|
try {
|
|
|
// Try adding and retrieving the keys in order to validate that the backup file is correct
|
|
|
keyring.addFromJson(accountJsonObj)
|
|
|
account = keyring.getPair(accountJsonObj.address) as NamedKeyringPair // We can be sure it's named, because we forced it before
|
|
|
} catch (e) {
|
|
|
- throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile })
|
|
|
+ throw new CLIError(`Provided backup file is not valid (${e.message})`, { exit: ExitCodes.InvalidFile })
|
|
|
}
|
|
|
|
|
|
return account
|
|
@@ -103,7 +174,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- fetchAccounts(includeSpecial = false): NamedKeyringPair[] {
|
|
|
+ fetchAccounts(): NamedKeyringPair[] {
|
|
|
let files: string[] = []
|
|
|
const accountDir = this.getAccountsDirPath()
|
|
|
try {
|
|
@@ -116,143 +187,268 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
|
|
|
return files
|
|
|
.map((fileName) => {
|
|
|
const filePath = path.join(accountDir, fileName)
|
|
|
- if (!includeSpecial && filePath.includes(SPECIAL_ACCOUNT_POSTFIX + '.')) return null
|
|
|
return this.fetchAccountOrNullFromFile(filePath)
|
|
|
})
|
|
|
- .filter((accObj) => accObj !== null) as NamedKeyringPair[]
|
|
|
+ .filter((account) => account !== null) as NamedKeyringPair[]
|
|
|
}
|
|
|
|
|
|
- getSelectedAccountFilename(): string {
|
|
|
- return this.getPreservedState().selectedAccountFilename
|
|
|
+ getPairs(includeDevAccounts = true): NamedKeyringPair[] {
|
|
|
+ return this.getKeyring()
|
|
|
+ .getPairs()
|
|
|
+ .filter((p) => includeDevAccounts || !p.meta.isTesting) as NamedKeyringPair[]
|
|
|
}
|
|
|
|
|
|
- getSelectedAccount(): NamedKeyringPair | null {
|
|
|
- const selectedAccountFilename = this.getSelectedAccountFilename()
|
|
|
-
|
|
|
- if (!selectedAccountFilename) {
|
|
|
- return null
|
|
|
- }
|
|
|
+ getPair(key: string): NamedKeyringPair {
|
|
|
+ return this.getKeyring().getPair(key) as NamedKeyringPair
|
|
|
+ }
|
|
|
|
|
|
- const account = this.fetchAccountOrNullFromFile(path.join(this.getAccountsDirPath(), selectedAccountFilename))
|
|
|
+ async getDecodedPair(key: string): Promise<NamedKeyringPair> {
|
|
|
+ const pair = this.getPair(key)
|
|
|
|
|
|
- return account
|
|
|
+ return (await this.requestPairDecoding(pair)) as NamedKeyringPair
|
|
|
}
|
|
|
|
|
|
- // Use when account usage is required in given command
|
|
|
- async getRequiredSelectedAccount(promptIfMissing = true): Promise<NamedKeyringPair> {
|
|
|
- let selectedAccount: NamedKeyringPair | null = this.getSelectedAccount()
|
|
|
- if (!selectedAccount) {
|
|
|
- if (!promptIfMissing) {
|
|
|
- this.error('No default account selected! Use account:choose to set the default account.', {
|
|
|
- exit: ExitCodes.NoAccountSelected,
|
|
|
- })
|
|
|
- }
|
|
|
+ async requestPairDecoding(pair: KeyringPair, message?: string): Promise<KeyringPair> {
|
|
|
+ // Skip if pair already unlocked
|
|
|
+ if (!pair.isLocked) {
|
|
|
+ return pair
|
|
|
+ }
|
|
|
|
|
|
- const accounts: NamedKeyringPair[] = this.fetchAccounts()
|
|
|
- if (!accounts.length) {
|
|
|
- this.error('No accounts available! Use account:import in order to import accounts into the CLI.', {
|
|
|
- exit: ExitCodes.NoAccountFound,
|
|
|
- })
|
|
|
- }
|
|
|
+ // First - try decoding using empty string
|
|
|
+ try {
|
|
|
+ pair.decodePkcs8('')
|
|
|
+ return pair
|
|
|
+ } catch (e) {
|
|
|
+ // Continue...
|
|
|
+ }
|
|
|
|
|
|
- this.warn('No default account selected!')
|
|
|
- selectedAccount = await this.promptForAccount(accounts)
|
|
|
- await this.setSelectedAccount(selectedAccount)
|
|
|
+ let isPassValid = false
|
|
|
+ while (!isPassValid) {
|
|
|
+ try {
|
|
|
+ const password = await this.promptForPassword(message)
|
|
|
+ pair.decodePkcs8(password)
|
|
|
+ isPassValid = true
|
|
|
+ } catch (e) {
|
|
|
+ this.warn('Invalid password... Try again.')
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return selectedAccount
|
|
|
+ return pair
|
|
|
}
|
|
|
|
|
|
- async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
|
|
|
- const accountFilename = fs.existsSync(this.getAccountFilePath(account, true))
|
|
|
- ? this.generateAccountFilename(account, true)
|
|
|
- : this.generateAccountFilename(account)
|
|
|
-
|
|
|
- await this.setPreservedState({ selectedAccountFilename: accountFilename })
|
|
|
+ initKeyring(): void {
|
|
|
+ this.keyring = this.getApi().isDevelopment
|
|
|
+ ? createDevelopmentKeyring(KEYRING_OPTIONS)
|
|
|
+ : new Keyring(KEYRING_OPTIONS)
|
|
|
+ const accounts = this.fetchAccounts()
|
|
|
+ accounts.forEach((a) => this.getKeyring().addPair(a))
|
|
|
}
|
|
|
|
|
|
- async promptForPassword(message = "Your account's password") {
|
|
|
- const { password } = await inquirer.prompt([{ name: 'password', type: 'password', message }])
|
|
|
+ async promptForPassword(message = "Your account's password"): Promise<string> {
|
|
|
+ const { password } = await inquirer.prompt([
|
|
|
+ {
|
|
|
+ name: 'password',
|
|
|
+ type: 'password',
|
|
|
+ message,
|
|
|
+ },
|
|
|
+ ])
|
|
|
|
|
|
return password
|
|
|
}
|
|
|
|
|
|
async promptForAccount(
|
|
|
- accounts: NamedKeyringPair[],
|
|
|
- defaultAccount: NamedKeyringPair | null = null,
|
|
|
message = 'Select an account',
|
|
|
+ createIfUnavailable = true,
|
|
|
+ includeDevAccounts = true,
|
|
|
showBalances = true
|
|
|
- ): Promise<NamedKeyringPair> {
|
|
|
- let balances: DeriveBalancesAll[]
|
|
|
+ ): Promise<string> {
|
|
|
+ const pairs = this.getPairs(includeDevAccounts)
|
|
|
+
|
|
|
+ if (!pairs.length) {
|
|
|
+ this.warn('No accounts available!')
|
|
|
+ if (createIfUnavailable) {
|
|
|
+ await this.requireConfirmation('Do you want to create a new account?', true)
|
|
|
+ pairs.push(await this.createAccount())
|
|
|
+ } else {
|
|
|
+ this.exit()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let balances: DeriveBalancesAll[] = []
|
|
|
if (showBalances) {
|
|
|
- balances = await this.getApi().getAccountsBalancesInfo(accounts.map((acc) => acc.address))
|
|
|
+ balances = await this.getApi().getAccountsBalancesInfo(pairs.map((p) => p.address))
|
|
|
}
|
|
|
- const longestAccNameLength: number = accounts.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0)
|
|
|
- const accNameColLength: number = Math.min(longestAccNameLength + 1, 20)
|
|
|
- const { chosenAccountFilename } = await inquirer.prompt([
|
|
|
- {
|
|
|
- name: 'chosenAccountFilename',
|
|
|
- message,
|
|
|
- type: 'list',
|
|
|
- choices: accounts.map((account: NamedKeyringPair, i) => ({
|
|
|
- name:
|
|
|
- `${toFixedLength(account.meta.name, accNameColLength)} | ` +
|
|
|
- `${account.address} | ` +
|
|
|
- ((showBalances || '') &&
|
|
|
- `${formatBalance(balances[i].availableBalance)} / ` + `${formatBalance(balances[i].votingBalance)}`),
|
|
|
- value: this.generateAccountFilename(account),
|
|
|
- short: `${account.meta.name} (${account.address})`,
|
|
|
- })),
|
|
|
- default: defaultAccount && this.generateAccountFilename(defaultAccount),
|
|
|
- },
|
|
|
- ])
|
|
|
|
|
|
- return accounts.find((acc) => this.generateAccountFilename(acc) === chosenAccountFilename) as NamedKeyringPair
|
|
|
+ const longestNameLen: number = pairs.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0)
|
|
|
+ const nameColLength: number = Math.min(longestNameLen + 1, 20)
|
|
|
+ const chosenKey = await this.simplePrompt({
|
|
|
+ message,
|
|
|
+ type: 'list',
|
|
|
+ choices: pairs.map((p, i) => ({
|
|
|
+ name:
|
|
|
+ `${toFixedLength(p.meta.name, nameColLength)} | ` +
|
|
|
+ `${p.address} | ` +
|
|
|
+ ((showBalances || '') &&
|
|
|
+ `${formatBalance(balances[i].availableBalance)} / ` + `${formatBalance(balances[i].votingBalance)}`),
|
|
|
+ value: p.address,
|
|
|
+ })),
|
|
|
+ })
|
|
|
+
|
|
|
+ return chosenKey
|
|
|
}
|
|
|
|
|
|
- async requestAccountDecoding(account: NamedKeyringPair): Promise<void> {
|
|
|
- // Skip if account already unlocked
|
|
|
- if (!account.isLocked) {
|
|
|
- return
|
|
|
- }
|
|
|
+ promptForCustomAddress(): Promise<string> {
|
|
|
+ return this.simplePrompt({
|
|
|
+ message: 'Provide custom address',
|
|
|
+ validate: (a) => validateAddress(a),
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- // First - try decoding using empty string
|
|
|
- try {
|
|
|
- account.decodePkcs8('')
|
|
|
- return
|
|
|
- } catch (e) {
|
|
|
- // Continue...
|
|
|
+ async promptForAnyAddress(message = 'Select an address'): Promise<string> {
|
|
|
+ const type: 'available' | 'new' | 'custom' = await this.simplePrompt({
|
|
|
+ message,
|
|
|
+ type: 'list',
|
|
|
+ choices: [
|
|
|
+ { name: 'Available account', value: 'available' },
|
|
|
+ { name: 'New account', value: 'new' },
|
|
|
+ { name: 'Custom address', value: 'custom' },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+
|
|
|
+ if (type === 'available') {
|
|
|
+ return this.promptForAccount()
|
|
|
+ } else if (type === 'new') {
|
|
|
+ return (await this.createAccount()).address
|
|
|
+ } else {
|
|
|
+ return this.promptForCustomAddress()
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- let isPassValid = false
|
|
|
- while (!isPassValid) {
|
|
|
- try {
|
|
|
- const password = await this.promptForPassword()
|
|
|
- account.decodePkcs8(password)
|
|
|
- isPassValid = true
|
|
|
- } catch (e) {
|
|
|
- this.warn('Invalid password... Try again.')
|
|
|
- }
|
|
|
+ async getRequiredMemberContext(): Promise<[MemberId, Membership]> {
|
|
|
+ // TODO: Limit only to a set of members provided by the user?
|
|
|
+ const allMembers = await this.getApi().allMembers()
|
|
|
+ const availableMembers = allMembers.filter(([, m]) => this.isKeyAvailable(m.controller_account.toString()))
|
|
|
+
|
|
|
+ if (!availableMembers.length) {
|
|
|
+ this.error('No member controller key available!', { exit: ExitCodes.AccessDenied })
|
|
|
+ } else if (availableMembers.length === 1) {
|
|
|
+ return availableMembers[0]
|
|
|
+ } else {
|
|
|
+ return this.promptForMember(availableMembers, 'Choose member context')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async getRequiredMemberId(): Promise<number> {
|
|
|
- const account = await this.getRequiredSelectedAccount()
|
|
|
- const memberIds = await this.getApi().getMemberIdsByControllerAccount(account.address)
|
|
|
- if (!memberIds.length) {
|
|
|
- this.error('Membership required to access this command!', { exit: ExitCodes.AccessDenied })
|
|
|
+ async promptForMember(
|
|
|
+ availableMembers: [MemberId, Membership][],
|
|
|
+ message = 'Choose a member'
|
|
|
+ ): Promise<[MemberId, Membership]> {
|
|
|
+ const memberIndex = await this.simplePrompt({
|
|
|
+ type: 'list',
|
|
|
+ message,
|
|
|
+ choices: availableMembers.map(([, m], i) => ({
|
|
|
+ name: m.handle_hash.toString(),
|
|
|
+ value: i,
|
|
|
+ })),
|
|
|
+ })
|
|
|
+
|
|
|
+ return availableMembers[memberIndex]
|
|
|
+ }
|
|
|
+
|
|
|
+ async promptForStakingAccount(stakeValue: BN, memberId: MemberId, member: Membership): Promise<string> {
|
|
|
+ this.log(`Required stake: ${formatBalance(stakeValue)}`)
|
|
|
+ let stakingAccount: string
|
|
|
+ while (true) {
|
|
|
+ stakingAccount = await this.promptForAnyAddress()
|
|
|
+ const { balances } = await this.getApi().getAccountSummary(stakingAccount)
|
|
|
+ const stakingStatus = await this.getApi().stakingAccountStatus(stakingAccount)
|
|
|
+
|
|
|
+ if (balances.lockedBalance.gtn(0)) {
|
|
|
+ this.warn('This account is already used for other staking purposes, choose different account...')
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (stakingStatus && !stakingStatus.member_id.eq(memberId)) {
|
|
|
+ this.warn('This account is already used as staking accout by other member, choose different account...')
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ let additionalStakingAccountCosts = new BN(0)
|
|
|
+ if (!stakingStatus || (stakingStatus && stakingStatus.confirmed.isFalse)) {
|
|
|
+ if (!this.isKeyAvailable(stakingAccount)) {
|
|
|
+ this.warn(
|
|
|
+ 'Account is not a confirmed staking account and cannot be directly accessed via CLI, choose different account...'
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ this.warn(
|
|
|
+ `This account is not a confirmed staking account. ` +
|
|
|
+ `Additional funds (fees) may be required to set it as a staking account.`
|
|
|
+ )
|
|
|
+ if (!stakingStatus) {
|
|
|
+ additionalStakingAccountCosts = await this.getApi().estimateFee(
|
|
|
+ await this.getDecodedPair(stakingAccount),
|
|
|
+ this.getOriginalApi().tx.members.addStakingAccountCandidate(memberId)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const requiredStakingAccountBalance = stakeValue.add(additionalStakingAccountCosts)
|
|
|
+ const missingStakingAccountBalance = requiredStakingAccountBalance.sub(balances.availableBalance)
|
|
|
+ if (missingStakingAccountBalance.gtn(0)) {
|
|
|
+ this.warn(
|
|
|
+ `Not enough available balance! Missing: ${chalk.cyan(formatBalance(missingStakingAccountBalance))}.` +
|
|
|
+ (additionalStakingAccountCosts.gtn(0)
|
|
|
+ ? ` (includes ${formatBalance(additionalStakingAccountCosts)} fee for setting new staking account)`
|
|
|
+ : '')
|
|
|
+ )
|
|
|
+ const transferTokens = await this.simplePrompt({
|
|
|
+ type: 'confirm',
|
|
|
+ message: `Do you want to transfer ${chalk.cyan(
|
|
|
+ formatBalance(missingStakingAccountBalance)
|
|
|
+ )} from another account?`,
|
|
|
+ })
|
|
|
+ if (transferTokens) {
|
|
|
+ const key = await this.promptForAccount('Choose source account')
|
|
|
+ await this.sendAndFollowNamedTx(await this.getDecodedPair(key), 'balances', 'transferKeepAlive', [
|
|
|
+ stakingAccount,
|
|
|
+ missingStakingAccountBalance,
|
|
|
+ ])
|
|
|
+ } else {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!stakingStatus) {
|
|
|
+ await this.sendAndFollowNamedTx(
|
|
|
+ await this.getDecodedPair(stakingAccount),
|
|
|
+ 'members',
|
|
|
+ 'addStakingAccountCandidate',
|
|
|
+ [memberId]
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!stakingStatus || stakingStatus.confirmed.isFalse) {
|
|
|
+ await this.sendAndFollowNamedTx(
|
|
|
+ await this.getDecodedPair(member.controller_account.toString()),
|
|
|
+ 'members',
|
|
|
+ 'confirmStakingAccount',
|
|
|
+ [memberId, stakingAccount]
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ break
|
|
|
}
|
|
|
|
|
|
- return memberIds[0].toNumber() // FIXME: Temporary solution (just using the first one)
|
|
|
+ return stakingAccount
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
await super.init()
|
|
|
try {
|
|
|
this.initAccountsFs()
|
|
|
- this.initSpecialAccounts()
|
|
|
} catch (e) {
|
|
|
throw this.createDataDirInitError()
|
|
|
}
|
|
|
+ await this.initKeyring()
|
|
|
}
|
|
|
}
|