Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
670 lines
19 KiB
Go
670 lines
19 KiB
Go
package chaincmd
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cmdrunner/step"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosver"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
commandStart = "start"
|
|
commandInit = "init"
|
|
commandKeys = "keys"
|
|
commandGenesis = "genesis"
|
|
commandAddGenesisAccount = "add-genesis-account"
|
|
commandGentx = "gentx"
|
|
commandCollectGentxs = "collect-gentxs"
|
|
commandValidateGenesis = "validate"
|
|
commandExportGenssis = "export"
|
|
commandShowNodeID = "show-node-id"
|
|
commandStatus = "status"
|
|
commandTx = "tx"
|
|
commandQuery = "query"
|
|
commandUnsafeReset = "unsafe-reset-all"
|
|
commandTendermint = "tendermint"
|
|
commandTestnetInPlace = "in-place-testnet"
|
|
commandTestnetMultiNode = "multi-node"
|
|
|
|
optionHome = "--home"
|
|
optionNode = "--node"
|
|
optionKeyringBackend = "--keyring-backend"
|
|
optionChainID = "--chain-id"
|
|
optionOutput = "--output"
|
|
optionRecover = "--recover"
|
|
optionAddress = "--address"
|
|
optionAmount = "--amount"
|
|
optionFees = "--fees"
|
|
optionValidatorMoniker = "--moniker"
|
|
optionValidatorCommissionRate = "--commission-rate"
|
|
optionValidatorCommissionMaxRate = "--commission-max-rate"
|
|
optionValidatorCommissionMaxChangeRate = "--commission-max-change-rate"
|
|
optionValidatorMinSelfDelegation = "--min-self-delegation"
|
|
optionValidatorGasPrices = "--gas-prices"
|
|
optionValidatorDetails = "--details"
|
|
optionValidatorIdentity = "--identity"
|
|
optionValidatorWebsite = "--website"
|
|
optionValidatorSecurityContact = "--security-contact"
|
|
optionYes = "--yes"
|
|
optionHomeClient = "--home-client"
|
|
optionCoinType = "--coin-type"
|
|
optionVestingAmount = "--vesting-amount"
|
|
optionVestingEndTime = "--vesting-end-time"
|
|
optionBroadcastMode = "--broadcast-mode"
|
|
optionAccount = "--account"
|
|
optionIndex = "--index"
|
|
optionValidatorPrivateKey = "--validator-privkey"
|
|
optionAccountToFund = "--accounts-to-fund"
|
|
optionSkipConfirmation = "--skip-confirmation"
|
|
optionAmountStakes = "--validators-stake-amount"
|
|
optionOutPutDir = "--output-dir"
|
|
optionNumValidator = "--v"
|
|
optionNodeDirPrefix = "--node-dir-prefix"
|
|
optionPorts = "--list-ports"
|
|
|
|
constTendermint = "tendermint"
|
|
constJSON = "json"
|
|
)
|
|
|
|
type KeyringBackend string
|
|
|
|
const (
|
|
KeyringBackendUnspecified KeyringBackend = ""
|
|
KeyringBackendOS KeyringBackend = "os"
|
|
KeyringBackendFile KeyringBackend = "file"
|
|
KeyringBackendPass KeyringBackend = "pass"
|
|
KeyringBackendTest KeyringBackend = "test"
|
|
KeyringBackendKwallet KeyringBackend = "kwallet"
|
|
)
|
|
|
|
type ChainCmd struct {
|
|
appCmd string
|
|
chainID string
|
|
homeDir string
|
|
keyringBackend KeyringBackend
|
|
keyringPassword string
|
|
nodeAddress string
|
|
|
|
isAutoChainIDDetectionEnabled bool
|
|
|
|
sdkVersion cosmosver.Version
|
|
}
|
|
|
|
// New creates a new ChainCmd to launch command with the chain app.
|
|
func New(appCmd string, options ...Option) ChainCmd {
|
|
chainCmd := ChainCmd{
|
|
appCmd: appCmd,
|
|
sdkVersion: cosmosver.Latest,
|
|
}
|
|
|
|
applyOptions(&chainCmd, options)
|
|
|
|
return chainCmd
|
|
}
|
|
|
|
// Copy makes a copy of ChainCmd by overwriting its options with given options.
|
|
func (c ChainCmd) Copy(options ...Option) ChainCmd {
|
|
applyOptions(&c, options)
|
|
|
|
return c
|
|
}
|
|
|
|
// Option configures ChainCmd.
|
|
type Option func(*ChainCmd)
|
|
|
|
func applyOptions(c *ChainCmd, options []Option) {
|
|
for _, apply := range options {
|
|
apply(c)
|
|
}
|
|
}
|
|
|
|
// WithVersion sets the version of the blockchain.
|
|
// when this is not provided, the latest version of SDK is assumed.
|
|
func WithVersion(v cosmosver.Version) Option {
|
|
return func(c *ChainCmd) {
|
|
c.sdkVersion = v
|
|
}
|
|
}
|
|
|
|
// WithHome replaces the default home used by the chain.
|
|
func WithHome(home string) Option {
|
|
return func(c *ChainCmd) {
|
|
c.homeDir = home
|
|
}
|
|
}
|
|
|
|
// WithChainID provides a specific chain ID for the commands that accept this option.
|
|
func WithChainID(chainID string) Option {
|
|
return func(c *ChainCmd) {
|
|
c.chainID = chainID
|
|
}
|
|
}
|
|
|
|
// WithAutoChainIDDetection finds out the chain id by communicating with the node running.
|
|
func WithAutoChainIDDetection() Option {
|
|
return func(c *ChainCmd) {
|
|
c.isAutoChainIDDetectionEnabled = true
|
|
}
|
|
}
|
|
|
|
// WithKeyringBackend provides a specific keyring backend for the commands that accept this option.
|
|
func WithKeyringBackend(keyringBackend KeyringBackend) Option {
|
|
return func(c *ChainCmd) {
|
|
c.keyringBackend = keyringBackend
|
|
}
|
|
}
|
|
|
|
// WithKeyringPassword provides a password to unlock keyring.
|
|
func WithKeyringPassword(password string) Option {
|
|
return func(c *ChainCmd) {
|
|
c.keyringPassword = password
|
|
}
|
|
}
|
|
|
|
// WithNodeAddress sets the node address for the commands that needs to make an
|
|
// API request to the node that has a different node address other than the default one.
|
|
func WithNodeAddress(addr string) Option {
|
|
return func(c *ChainCmd) {
|
|
c.nodeAddress = addr
|
|
}
|
|
}
|
|
|
|
// Name returns the app name (prefix of the chain daemon).
|
|
func (c ChainCmd) Name() string {
|
|
return c.appCmd
|
|
}
|
|
|
|
// StartCommand returns the command to start the daemon of the chain.
|
|
func (c ChainCmd) StartCommand(options ...string) step.Option {
|
|
command := append([]string{
|
|
commandStart,
|
|
}, options...)
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// InitCommand returns the command to initialize the chain.
|
|
func (c ChainCmd) InitCommand(moniker string, options ...string) step.Option {
|
|
command := append([]string{
|
|
commandInit,
|
|
moniker,
|
|
}, options...)
|
|
command = c.attachChainID(command)
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// AddKeyCommand returns the command to add a new key in the chain keyring.
|
|
func (c ChainCmd) AddKeyCommand(accountName, coinType, accountNumber, addressIndex string) step.Option {
|
|
command := []string{
|
|
commandKeys,
|
|
"add",
|
|
accountName,
|
|
optionOutput,
|
|
constJSON,
|
|
}
|
|
if coinType != "" {
|
|
command = append(command, optionCoinType, coinType)
|
|
}
|
|
if accountNumber != "" {
|
|
command = append(command, optionAccount, accountNumber)
|
|
}
|
|
if addressIndex != "" {
|
|
command = append(command, optionIndex, addressIndex)
|
|
}
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// RecoverKeyCommand returns the command to recover a key into the chain keyring from a mnemonic.
|
|
func (c ChainCmd) RecoverKeyCommand(accountName, coinType, accountNumber, addressIndex string) step.Option {
|
|
command := []string{
|
|
commandKeys,
|
|
"add",
|
|
accountName,
|
|
optionRecover,
|
|
}
|
|
if coinType != "" {
|
|
command = append(command, optionCoinType, coinType)
|
|
}
|
|
if accountNumber != "" {
|
|
command = append(command, optionAccount, accountNumber)
|
|
}
|
|
if addressIndex != "" {
|
|
command = append(command, optionIndex, addressIndex)
|
|
}
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// ImportKeyCommand returns the command to import a key into the chain keyring from a key file.
|
|
func (c ChainCmd) ImportKeyCommand(accountName, keyFile string) step.Option {
|
|
command := []string{
|
|
commandKeys,
|
|
"import",
|
|
accountName,
|
|
keyFile,
|
|
}
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// ShowKeyAddressCommand returns the command to print the address of a key in the chain keyring.
|
|
func (c ChainCmd) ShowKeyAddressCommand(accountName string) step.Option {
|
|
command := []string{
|
|
commandKeys,
|
|
"show",
|
|
accountName,
|
|
optionAddress,
|
|
}
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// ListKeysCommand returns the command to print the list of a keys in the chain keyring.
|
|
func (c ChainCmd) ListKeysCommand() step.Option {
|
|
command := []string{
|
|
commandKeys,
|
|
"list",
|
|
optionOutput,
|
|
constJSON,
|
|
}
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// AddGenesisAccountCommand returns the command to add a new account in the genesis file of the chain.
|
|
func (c ChainCmd) AddGenesisAccountCommand(address, coins string) step.Option {
|
|
command := []string{
|
|
commandGenesis,
|
|
commandAddGenesisAccount,
|
|
address,
|
|
coins,
|
|
}
|
|
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// AddVestingAccountCommand returns the command to add a delayed vesting account in the genesis file of the chain.
|
|
func (c ChainCmd) AddVestingAccountCommand(address, originalCoins, vestingCoins string, vestingEndTime int64) step.Option {
|
|
command := []string{
|
|
commandGenesis,
|
|
commandAddGenesisAccount,
|
|
address,
|
|
originalCoins,
|
|
optionVestingAmount,
|
|
vestingCoins,
|
|
optionVestingEndTime,
|
|
fmt.Sprintf("%d", vestingEndTime),
|
|
}
|
|
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// GentxOption for the GentxCommand.
|
|
type GentxOption func([]string) []string
|
|
|
|
// GentxWithMoniker provides moniker option for the gentx command.
|
|
func GentxWithMoniker(moniker string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(moniker) > 0 {
|
|
return append(command, optionValidatorMoniker, moniker)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithCommissionRate provides commission rate option for the gentx command.
|
|
func GentxWithCommissionRate(commissionRate string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(commissionRate) > 0 {
|
|
return append(command, optionValidatorCommissionRate, commissionRate)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithCommissionMaxRate provides commission max rate option for the gentx command.
|
|
func GentxWithCommissionMaxRate(commissionMaxRate string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(commissionMaxRate) > 0 {
|
|
return append(command, optionValidatorCommissionMaxRate, commissionMaxRate)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithCommissionMaxChangeRate provides commission max change rate option for the gentx command.
|
|
func GentxWithCommissionMaxChangeRate(commissionMaxChangeRate string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(commissionMaxChangeRate) > 0 {
|
|
return append(command, optionValidatorCommissionMaxChangeRate, commissionMaxChangeRate)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithMinSelfDelegation provides minimum self delegation option for the gentx command.
|
|
func GentxWithMinSelfDelegation(minSelfDelegation string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(minSelfDelegation) > 0 {
|
|
return append(command, optionValidatorMinSelfDelegation, minSelfDelegation)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithGasPrices provides gas price option for the gentx command.
|
|
func GentxWithGasPrices(gasPrices string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(gasPrices) > 0 {
|
|
return append(command, optionValidatorGasPrices, gasPrices)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithDetails provides validator details option for the gentx command.
|
|
func GentxWithDetails(details string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(details) > 0 {
|
|
return append(command, optionValidatorDetails, details)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithIdentity provides validator identity option for the gentx command.
|
|
func GentxWithIdentity(identity string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(identity) > 0 {
|
|
return append(command, optionValidatorIdentity, identity)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithWebsite provides validator website option for the gentx command.
|
|
func GentxWithWebsite(website string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(website) > 0 {
|
|
return append(command, optionValidatorWebsite, website)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// GentxWithSecurityContact provides validator security contact option for the gentx command.
|
|
func GentxWithSecurityContact(securityContact string) GentxOption {
|
|
return func(command []string) []string {
|
|
if len(securityContact) > 0 {
|
|
return append(command, optionValidatorSecurityContact, securityContact)
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
func (c ChainCmd) IsAutoChainIDDetectionEnabled() bool {
|
|
return c.isAutoChainIDDetectionEnabled
|
|
}
|
|
|
|
func (c ChainCmd) SDKVersion() cosmosver.Version {
|
|
return c.sdkVersion
|
|
}
|
|
|
|
// GentxCommand returns the command to generate a gentx for the chain.
|
|
func (c ChainCmd) GentxCommand(
|
|
validatorName string,
|
|
selfDelegation string,
|
|
options ...GentxOption,
|
|
) step.Option {
|
|
command := []string{
|
|
commandGenesis,
|
|
commandGentx,
|
|
}
|
|
|
|
switch {
|
|
case c.sdkVersion.LT(cosmosver.StargateFortyVersion):
|
|
command = append(command,
|
|
validatorName,
|
|
optionAmount,
|
|
selfDelegation,
|
|
)
|
|
case c.sdkVersion.GTE(cosmosver.StargateFortyVersion):
|
|
command = append(command,
|
|
validatorName,
|
|
selfDelegation,
|
|
)
|
|
}
|
|
|
|
// Apply the options provided by the user
|
|
for _, apply := range options {
|
|
command = apply(command)
|
|
}
|
|
|
|
command = c.attachChainID(command)
|
|
command = c.attachKeyringBackend(command)
|
|
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// CollectGentxsCommand returns the command to gather the gentxs in /gentx dir into the genesis file of the chain.
|
|
func (c ChainCmd) CollectGentxsCommand() step.Option {
|
|
command := []string{
|
|
commandGenesis,
|
|
commandCollectGentxs,
|
|
}
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// ValidateGenesisCommand returns the command to check the validity of the chain genesis.
|
|
func (c ChainCmd) ValidateGenesisCommand() step.Option {
|
|
command := []string{
|
|
commandGenesis,
|
|
commandValidateGenesis,
|
|
}
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// ShowNodeIDCommand returns the command to print the node ID of the node for the chain.
|
|
func (c ChainCmd) ShowNodeIDCommand() step.Option {
|
|
command := []string{
|
|
constTendermint,
|
|
commandShowNodeID,
|
|
}
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// UnsafeResetCommand returns the command to reset the blockchain database.
|
|
func (c ChainCmd) UnsafeResetCommand() step.Option {
|
|
var command []string
|
|
|
|
if c.sdkVersion.GTE(cosmosver.StargateFortyFiveThreeVersion) {
|
|
command = append(command, commandTendermint)
|
|
}
|
|
|
|
command = append(command, commandUnsafeReset)
|
|
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// ExportCommand returns the command to export the state of the blockchain into a genesis file.
|
|
func (c ChainCmd) ExportCommand() step.Option {
|
|
command := []string{
|
|
commandExportGenssis,
|
|
}
|
|
return c.daemonCommand(command)
|
|
}
|
|
|
|
// BankSendOption for the BankSendCommand.
|
|
type BankSendOption func([]string) []string
|
|
|
|
// BankSendWithFees sets fees to pay along with transaction for the bank send command.
|
|
func BankSendWithFees(fee sdk.Coin) BankSendOption {
|
|
return func(command []string) []string {
|
|
if !fee.IsNil() {
|
|
return append(command, optionFees, fee.String())
|
|
}
|
|
return command
|
|
}
|
|
}
|
|
|
|
// BankSendCommand returns the command for transferring tokens.
|
|
func (c ChainCmd) BankSendCommand(fromAddress, toAddress, amount string, options ...BankSendOption) step.Option {
|
|
command := []string{
|
|
commandTx,
|
|
}
|
|
|
|
command = append(command, "bank")
|
|
command = append(command,
|
|
"send",
|
|
fromAddress,
|
|
toAddress,
|
|
amount,
|
|
)
|
|
command = append(command,
|
|
optionBroadcastMode, flags.BroadcastSync,
|
|
optionYes,
|
|
)
|
|
|
|
// Apply the options provided by the user
|
|
for _, apply := range options {
|
|
command = apply(command)
|
|
}
|
|
|
|
command = c.attachChainID(command)
|
|
command = c.attachKeyringBackend(command)
|
|
command = c.attachNode(command)
|
|
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// QueryTxCommand returns the command to query tx.
|
|
func (c ChainCmd) QueryTxCommand(txHash string) step.Option {
|
|
command := []string{
|
|
commandQuery,
|
|
"tx",
|
|
txHash,
|
|
}
|
|
|
|
command = c.attachNode(command)
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// QueryTxEventsCommand returns the command to query events.
|
|
func (c ChainCmd) QueryTxEventsCommand(query string) step.Option {
|
|
command := []string{
|
|
commandQuery,
|
|
"txs",
|
|
"--query",
|
|
query,
|
|
"--page", "1",
|
|
"--limit", "1000",
|
|
"--output", "json",
|
|
}
|
|
|
|
command = c.attachNode(command)
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// QueryTxQueryCommand returns the command to query tx.
|
|
func (c ChainCmd) QueryTxQueryCommand(query string) step.Option {
|
|
command := []string{
|
|
commandQuery,
|
|
"txs",
|
|
"--query",
|
|
query,
|
|
"--page", "1",
|
|
"--limit", "1000",
|
|
"--output", "json",
|
|
}
|
|
|
|
command = c.attachNode(command)
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// StatusCommand returns the command that fetches node's status.
|
|
func (c ChainCmd) StatusCommand() step.Option {
|
|
command := []string{
|
|
commandStatus,
|
|
}
|
|
|
|
command = c.attachNode(command)
|
|
return c.cliCommand(command)
|
|
}
|
|
|
|
// KeyringBackend returns the underlying keyring backend.
|
|
func (c ChainCmd) KeyringBackend() KeyringBackend {
|
|
return c.keyringBackend
|
|
}
|
|
|
|
// KeyringPassword returns the underlying keyring password.
|
|
func (c ChainCmd) KeyringPassword() string {
|
|
return c.keyringPassword
|
|
}
|
|
|
|
// attachChainID appends the chain ID flag to the provided command.
|
|
func (c ChainCmd) attachChainID(command []string) []string {
|
|
if c.chainID != "" {
|
|
command = append(command, []string{optionChainID, c.chainID}...)
|
|
}
|
|
return command
|
|
}
|
|
|
|
// attachKeyringBackend appends the keyring backend flag to the provided command.
|
|
func (c ChainCmd) attachKeyringBackend(command []string) []string {
|
|
if c.keyringBackend != "" {
|
|
command = append(command, []string{optionKeyringBackend, string(c.keyringBackend)}...)
|
|
}
|
|
return command
|
|
}
|
|
|
|
// attachHome appends the home flag to the provided command.
|
|
func (c ChainCmd) attachHome(command []string) []string {
|
|
if c.homeDir != "" {
|
|
command = append(command, []string{optionHome, c.homeDir}...)
|
|
}
|
|
return command
|
|
}
|
|
|
|
// attachNode appends the node flag to the provided command.
|
|
func (c ChainCmd) attachNode(command []string) []string {
|
|
if c.nodeAddress != "" {
|
|
command = append(command, []string{optionNode, c.nodeAddress}...)
|
|
}
|
|
return command
|
|
}
|
|
|
|
// daemonCommand returns the daemon command from the provided command.
|
|
func (c ChainCmd) daemonCommand(command []string) step.Option {
|
|
return step.Exec(c.appCmd, c.attachHome(command)...)
|
|
}
|
|
|
|
// cliCommand returns the cli command from the provided command.
|
|
func (c ChainCmd) cliCommand(command []string) step.Option {
|
|
return step.Exec(c.appCmd, c.attachHome(command)...)
|
|
}
|
|
|
|
// KeyringBackendFromString returns the keyring backend from its string.
|
|
func KeyringBackendFromString(kb string) (KeyringBackend, error) {
|
|
existingKeyringBackend := map[KeyringBackend]bool{
|
|
KeyringBackendUnspecified: true,
|
|
KeyringBackendOS: true,
|
|
KeyringBackendFile: true,
|
|
KeyringBackendPass: true,
|
|
KeyringBackendTest: true,
|
|
KeyringBackendKwallet: true,
|
|
}
|
|
|
|
if _, ok := existingKeyringBackend[KeyringBackend(kb)]; ok {
|
|
return KeyringBackend(kb), nil
|
|
}
|
|
return KeyringBackendUnspecified, errors.Errorf("unrecognized keyring backend: %s", kb)
|
|
}
|