Files

292 lines
7.5 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchaincmd
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/key"
"github.com/luxfi/cli/v2/pkg/keychain"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/cli/v2/pkg/ux"
)
func promptOwners(
kc *keychain.Keychain,
controlKeys []string,
sameControlKey bool,
threshold uint32,
subnetAuthKeys []string,
creatingBlockchain bool,
) ([]string, uint32, error) {
var err error
// accept only one control keys specification
if len(controlKeys) > 0 && sameControlKey {
return nil, 0, errMutuallyExlusiveControlKeys
}
// use first fee-paying key as control key
if sameControlKey {
kcKeys, err := kc.PChainFormattedStrAddresses()
if err != nil {
return nil, 0, err
}
if len(kcKeys) == 0 {
return nil, 0, fmt.Errorf("no keys found on keychain")
}
controlKeys = kcKeys[:1]
}
// prompt for control keys
if controlKeys == nil {
var cancelled bool
controlKeys, cancelled, err = getControlKeys(kc, creatingBlockchain)
if err != nil {
return nil, 0, err
}
if cancelled {
ux.Logger.PrintToUser("User cancelled. No operation was performed")
return nil, 0, fmt.Errorf("user cancelled operation")
}
}
ux.Logger.PrintToUser("Your blockchain control keys: %s", controlKeys)
// validate and prompt for threshold
if threshold == 0 && subnetAuthKeys != nil {
threshold = uint32(len(subnetAuthKeys))
}
if threshold > uint32(len(controlKeys)) {
return nil, 0, fmt.Errorf("given threshold is greater than number of control keys")
}
if threshold == 0 {
threshold, err = getThreshold(len(controlKeys))
if err != nil {
return nil, 0, err
}
}
return controlKeys, threshold, nil
}
func getControlKeys(kc *keychain.Keychain, creatingBlockchain bool) ([]string, bool, error) {
controlKeysInitialPrompt := "Configure which addresses may make changes to the blockchain.\n" +
"These addresses are known as your control keys. You will also\n" +
"set how many control keys are required to make a blockchain change (the threshold)."
ux.Logger.PrintToUser(controlKeysInitialPrompt)
if creatingBlockchain {
return getControlKeysForDeploy(kc)
} else {
return getControlKeysForChangeOwner(kc.Network)
}
}
func getControlKeysForDeploy(kc *keychain.Keychain) ([]string, bool, error) {
moreKeysPrompt := "How would you like to set your control keys?"
const (
useAll = "Use all stored keys"
custom = "Custom list"
)
var feePaying string
var listOptions []string
if kc.UsesLedger {
feePaying = "Use ledger address"
} else {
feePaying = "Use fee-paying key"
}
if kc.Network.Kind == models.Mainnet {
listOptions = []string{feePaying, custom}
} else {
listOptions = []string{feePaying, useAll, custom}
}
listDecision, err := app.Prompt.CaptureList(moreKeysPrompt, listOptions)
if err != nil {
return nil, false, err
}
var (
keys []string
cancelled bool
)
switch listDecision {
case feePaying:
var kcKeys []string
kcKeys, err = kc.PChainFormattedStrAddresses()
if err != nil {
return nil, false, err
}
if len(kcKeys) == 0 {
return nil, false, fmt.Errorf("no keys found on keychain")
}
keys = kcKeys[:1]
case useAll:
keys, err = useAllKeys(kc.Network)
case custom:
keys, cancelled, err = enterCustomKeys(kc.Network)
}
if err != nil {
return nil, false, err
}
if cancelled {
return nil, true, nil
}
return keys, false, nil
}
func getControlKeysForChangeOwner(network models.Network) ([]string, bool, error) {
moreKeysPrompt := "Which control keys would you like to set as the new blockchain owners?"
const (
getFromStored = "Get address from an existing stored key (created from lux key create or lux key import)"
custom = "Custom"
)
listOptions := []string{getFromStored, custom}
listDecision, err := app.Prompt.CaptureList(moreKeysPrompt, listOptions)
if err != nil {
return nil, false, err
}
var (
keys []string
cancelled bool
)
switch listDecision {
case getFromStored:
key, err := prompts.CaptureKeyAddress(
app.Prompt,
"be set as a control key",
app.GetKeyDir(),
app.GetKey,
network,
prompts.PChainFormat,
)
if err != nil {
return nil, false, err
}
keys = []string{key}
case custom:
keys, cancelled, err = enterCustomKeys(network)
}
if err != nil {
return nil, false, err
}
if cancelled {
return nil, true, nil
}
return keys, false, nil
}
func useAllKeys(network models.Network) ([]string, error) {
existing := []string{}
files, err := os.ReadDir(app.GetKeyDir())
if err != nil {
return nil, err
}
keyPaths := make([]string, 0, len(files))
for _, f := range files {
if strings.HasSuffix(f.Name(), constants.KeySuffix) {
keyPaths = append(keyPaths, filepath.Join(app.GetKeyDir(), f.Name()))
}
}
for _, kp := range keyPaths {
k, err := key.LoadSoft(network.ID, kp)
if err != nil {
return nil, err
}
existing = append(existing, k.P()...)
}
return existing, nil
}
func enterCustomKeys(network models.Network) ([]string, bool, error) {
controlKeysPrompt := "Enter control keys"
for {
// ask in a loop so that if some condition is not met we can keep asking
controlKeys, cancelled, err := controlKeysLoop(controlKeysPrompt, network)
if err != nil {
return nil, false, err
}
if cancelled {
return nil, cancelled, nil
}
if len(controlKeys) != 0 {
return controlKeys, false, nil
}
ux.Logger.PrintToUser("This tool does not allow to proceed without any control key set")
}
}
// controlKeysLoop asks as many controlkeys the user requires, until Done or Cancel is selected
func controlKeysLoop(controlKeysPrompt string, network models.Network) ([]string, bool, error) {
label := "Control key"
info := "Control keys are P-Chain addresses which have admin rights on the subnet.\n" +
"Only private keys which control such addresses are allowed to make changes on the subnet"
customPrompt := "Enter P-Chain address (Example: P-...)"
return prompts.CaptureListDecision(
// we need this to be able to mock test
app.Prompt,
// the main prompt for entering address keys
controlKeysPrompt,
// the Capture function to use
func(_ string) (string, error) {
return prompts.PromptAddress(
app.Prompt,
"be set as a control key",
app.GetKeyDir(),
app.GetKey,
"",
network,
prompts.PChainFormat,
customPrompt,
)
},
// the prompt for each address
"",
// label describes the entity we are prompting for (e.g. address, control key, etc.)
label,
// optional parameter to allow the user to print the info string for more information
info,
)
}
// getThreshold prompts for the threshold of addresses as a number
func getThreshold(maxLen int) (uint32, error) {
if maxLen == 1 {
return uint32(1), nil
}
// create a list of indexes so the user only has the option to choose what is the threshold
// instead of entering
indexList := make([]string, maxLen)
for i := 0; i < maxLen; i++ {
indexList[i] = strconv.Itoa(i + 1)
}
threshold, err := app.Prompt.CaptureList("Select required number of control key signatures to make a blockchain change", indexList)
if err != nil {
return 0, err
}
intTh, err := strconv.ParseUint(threshold, 0, 32)
if err != nil {
return 0, err
}
// this now should technically not happen anymore, but let's leave it as a double stitch
if intTh > uint64(maxLen) {
return 0, fmt.Errorf("the threshold can't be bigger than the number of control keys")
}
return uint32(intTh), err
}