package chaincmdrunner import ( "context" "encoding/json" "fmt" "os" "strings" "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/v29/ignite/pkg/errors" ) var ( // ErrAccountAlreadyExists returned when an already exists account attempted to be imported. ErrAccountAlreadyExists = errors.New("account already exists") // ErrAccountDoesNotExist returned when account does not exit. ErrAccountDoesNotExist = errors.New("account does not exit") ) const msgEmptyKeyring = "No records were found in keyring" // Account represents a user account. type Account struct { Name string `json:"name"` Address string `json:"address"` Mnemonic string `json:"mnemonic,omitempty"` } // AddAccount creates a new account or imports an account when mnemonic is provided. // returns with an error if the operation went unsuccessful or an account with the provided name // already exists. func (r Runner) AddAccount( ctx context.Context, name, mnemonic, coinType, accountNumber, addressIndex string, ) (Account, error) { if err := r.CheckAccountExist(ctx, name); err != nil { return Account{}, err } b := newBuffer() account := Account{ Name: name, Mnemonic: mnemonic, } // import the account when mnemonic is provided, otherwise create a new one. if mnemonic != "" { input := newBuffer() _, err := fmt.Fprintln(input, mnemonic) if err != nil { return Account{}, err } if r.chainCmd.KeyringPassword() != "" { _, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword()) if err != nil { return Account{}, err } _, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword()) if err != nil { return Account{}, err } } if err := r.run( ctx, runOptions{}, r.chainCmd.RecoverKeyCommand(name, coinType, accountNumber, addressIndex), step.Write(input.Bytes()), ); err != nil { return Account{}, err } } else { if err := r.run(ctx, runOptions{ stdout: b, stderr: b, stdin: os.Stdin, }, r.chainCmd.AddKeyCommand(name, coinType, accountNumber, addressIndex)); err != nil { return Account{}, err } data, err := b.JSONEnsuredBytes() if err != nil { return Account{}, err } if err := json.Unmarshal(data, &account); err != nil { return Account{}, err } } // get the address of the account. retrieved, err := r.ShowAccount(ctx, name) if err != nil { return Account{}, err } account.Address = retrieved.Address return account, nil } // ImportAccount import an account from a key file. func (r Runner) ImportAccount(ctx context.Context, name, keyFile, passphrase string) (Account, error) { if err := r.CheckAccountExist(ctx, name); err != nil { return Account{}, err } // write the passphrase as input // TODO: manage keyring backend other than test input := newBuffer() _, err := fmt.Fprintln(input, passphrase) if err != nil { return Account{}, err } if err := r.run( ctx, runOptions{}, r.chainCmd.ImportKeyCommand(name, keyFile), step.Write(input.Bytes()), ); err != nil { return Account{}, err } return r.ShowAccount(ctx, name) } // ListAccounts returns the list of accounts in the keyring. func (r Runner) ListAccounts(ctx context.Context) ([]Account, error) { // Get a JSON string with all accounts in the keyring b := newBuffer() if err := r.run(ctx, runOptions{stdout: b}, r.chainCmd.ListKeysCommand()); err != nil { return nil, err } // Make sure that the command output is not the empty keyring message. // This need to be checked because when the keyring is empty the command // finishes with exit code 0 and a plain text message. // This behavior was added to Cosmos SDK v0.46.2. See the link // https://github.com/cosmos/cosmos-sdk/blob/d01aa5b4a8/client/keys/list.go#L37 if strings.TrimSpace(b.String()) == msgEmptyKeyring { return nil, nil } data, err := b.JSONEnsuredBytes() if err != nil { return nil, err } var accounts []Account if err := json.Unmarshal(data, &accounts); err != nil { return nil, err } return accounts, nil } // CheckAccountExist returns an error if the account already exists in the chain keyring. func (r Runner) CheckAccountExist(ctx context.Context, name string) error { accounts, err := r.ListAccounts(ctx) if err != nil { return err } // Search for the account name for _, account := range accounts { if account.Name == name { return ErrAccountAlreadyExists } } return nil } // ShowAccount shows details of an account. func (r Runner) ShowAccount(ctx context.Context, name string) (Account, error) { b := newBuffer() opt := []step.Option{ r.chainCmd.ShowKeyAddressCommand(name), } if r.chainCmd.KeyringPassword() != "" { input := newBuffer() _, err := fmt.Fprintln(input, r.chainCmd.KeyringPassword()) if err != nil { return Account{}, err } opt = append(opt, step.Write(input.Bytes())) } if err := r.run(ctx, runOptions{stdout: b}, opt...); err != nil { if strings.Contains(err.Error(), "item could not be found") || strings.Contains(err.Error(), "not a valid name or address") { return Account{}, ErrAccountDoesNotExist } return Account{}, err } return Account{ Name: name, Address: strings.TrimSpace(b.String()), }, nil } // AddGenesisAccount adds account to genesis by its address. func (r Runner) AddGenesisAccount(ctx context.Context, address, coins string) error { return r.run(ctx, runOptions{}, r.chainCmd.AddGenesisAccountCommand(address, coins)) } // AddVestingAccount adds vesting account to genesis by its address. func (r Runner) AddVestingAccount( ctx context.Context, address, originalCoins, vestingCoins string, vestingEndTime int64, ) error { return r.run(ctx, runOptions{}, r.chainCmd.AddVestingAccountCommand(address, originalCoins, vestingCoins, vestingEndTime)) }