add alias commands

This commit is contained in:
Tyler Sturos 2025-04-26 13:22:55 -08:00
parent c28a430e8c
commit 165d4440ce
13 changed files with 398 additions and 20 deletions

126
client/cmd/config/alias.go Normal file
View File

@ -0,0 +1,126 @@
package config
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var ClientConfigAliasCmd = &cobra.Command{
Use: "alias",
Short: "Manage address aliases",
Long: `Manage the list of address aliases in the configuration.
For more information on how to use aliases, see the https://docs.quilibrium.com/docs/run-node/qclient/commands/alias.
Examples:
# Add a new address to the configuration
qclient config alias add my-friend 0x1234567890abcdef
# List all saved addresses
qclient config alias list
# Update an existing address
qclient config alias update my-friend 0xabcdef1234567890
# Remove an address from the configuration
qclient config alias remove my-friend`,
}
var addAddressCmd = &cobra.Command{
Use: "add [name] [address]",
Short: "Add a new address alias",
Long: `Add a new address alias to the configuration with a given name.`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
address := args[1]
if ClientConfig.AddressList == nil {
ClientConfig.AddressList = make(map[string]string)
}
if _, exists := ClientConfig.AddressList[name]; exists {
fmt.Printf("Alias for %s already exists. Use 'update' to modify it.\n", name)
os.Exit(1)
}
ClientConfig.AddressList[name] = address
err := utils.SaveClientConfig(ClientConfig)
if err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
fmt.Printf("Added alias %s for %s\n", address, name)
},
}
var listAddressesCmd = &cobra.Command{
Use: "list",
Short: "List all aliases",
Long: `List all aliases in the configuration.`,
Run: func(cmd *cobra.Command, args []string) {
if len(ClientConfig.AddressList) == 0 {
fmt.Println("No aliases found in configuration.")
return
}
fmt.Println("Address Aliases:")
for name, address := range ClientConfig.AddressList {
fmt.Printf(" %s -> %s\n", name, address)
}
},
}
var updateAddressCmd = &cobra.Command{
Use: "update [name] [address]",
Short: "Update an existing alias",
Long: `Update an existing alias in the configuration for a given name.`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
address := args[1]
if _, exists := ClientConfig.AddressList[name]; !exists {
fmt.Printf("Alias for %s does not exist.\n", name)
os.Exit(1)
}
ClientConfig.AddressList[name] = address
err := utils.SaveClientConfig(ClientConfig)
if err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
fmt.Printf("Updated address for %s to %s\n", name, address)
},
}
var deleteAddressCmd = &cobra.Command{
Use: "delete [name]",
Short: "Delete an alias",
Long: `Delete an alias from the configuration by name.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
if _, exists := ClientConfig.AddressList[name]; !exists {
fmt.Printf("Alias for %s does not exist.\n", name)
os.Exit(1)
}
delete(ClientConfig.AddressList, name)
err := utils.SaveClientConfig(ClientConfig)
if err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
fmt.Printf("Deleted alias for %s\n", name)
},
}
func init() {
ClientConfigAliasCmd.AddCommand(addAddressCmd)
ClientConfigAliasCmd.AddCommand(listAddressesCmd)
ClientConfigAliasCmd.AddCommand(updateAddressCmd)
ClientConfigAliasCmd.AddCommand(deleteAddressCmd)
}

View File

@ -1,12 +1,31 @@
package config
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var ClientConfig *utils.ClientConfig
var ConfigCmd = &cobra.Command{
Use: "config",
Short: "Performs a QClient configuration operation",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
parent := cmd.Parent()
if parent != nil && parent.PersistentPreRun != nil {
parent.PersistentPreRun(parent, args)
}
var err error
ClientConfig, err = utils.LoadClientConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
},
}
func init() {
@ -15,4 +34,5 @@ func init() {
ConfigCmd.AddCommand(ClientConfigPublicRpcCmd)
ConfigCmd.AddCommand(ClientConfigSetCustomRpcCmd)
ConfigCmd.AddCommand(ClientConfigSignatureCheckCmd)
ConfigCmd.AddCommand(ClientConfigAliasCmd)
}

View File

@ -218,5 +218,5 @@ func decodeHexString(hexStr string) ([]byte, error) {
}
func init() {
CrossMintCmd.PersistentFlags().StringVar(&ConfigDirectory, "config", "default", "config directory")
CrossMintCmd.PersistentFlags().StringVar(&ConfigDirectory, "config", utils.ReservedDefaultConfigName, "config directory")
}

View File

@ -27,7 +27,7 @@ Examples:
if len(args) > 0 {
NodeGetInfo(args[0])
} else {
NodeGetInfo("default")
NodeGetInfo(utils.ReservedDefaultConfigName)
}
},
}

View File

@ -41,7 +41,7 @@ The third example will create a new configuration at %s/myconfig and symlink it
}
// Check if trying to use "default" which is reserved for the symlink
if configName == "default" {
if configName == utils.ReservedDefaultConfigName {
fmt.Println("Error: 'default' is reserved for the symlink. Please use a different name.")
os.Exit(1)
}
@ -72,5 +72,5 @@ The third example will create a new configuration at %s/myconfig and symlink it
}
func init() {
NodeConfigCreateCmd.Flags().BoolVarP(&SetDefault, "default", "d", false, "Select this config as the default")
NodeConfigCreateCmd.Flags().BoolVarP(&SetDefault, utils.ReservedDefaultConfigName, "d", false, "Select this config as the default")
}

View File

@ -53,7 +53,7 @@ This will symlink %s/mynode to %s`, ConfigDirs, NodeConfigToRun),
name = configs[choice-1]
}
if name == "default" {
if name == utils.ReservedDefaultConfigName {
fmt.Println("Invalid configuration name. The 'default' is reserved for the default configuration.")
os.Exit(1)
}

View File

@ -2,6 +2,8 @@ package nodeconfig
import (
"os"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
func ListConfigurations() ([]string, error) {
@ -12,7 +14,7 @@ func ListConfigurations() ([]string, error) {
configs := make([]string, 0)
for _, file := range files {
if file.IsDir() && file.Name() != "default" {
if file.IsDir() && file.Name() != utils.ReservedDefaultConfigName {
configs = append(configs, file.Name())
}
}

View File

@ -3,7 +3,6 @@ package token
import (
"fmt"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
@ -12,13 +11,10 @@ var AccountCmd = &cobra.Command{
Use: "account",
Short: "Shows the account address of the managing account",
Run: func(cmd *cobra.Command, args []string) {
peerId := utils.GetPeerIDFromConfig(NodeConfig)
addr, err := poseidon.HashBytes([]byte(peerId))
account, err := utils.GetAccountFromNodeConfig(NodeConfig)
if err != nil {
panic(err)
}
addrBytes := addr.FillBytes(make([]byte, 32))
fmt.Printf("Account: 0x%x\n", addrBytes)
fmt.Printf("Account: 0x%x\n", account)
},
}

View File

@ -1,7 +1,15 @@
package token
import (
"bytes"
"context"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
@ -9,6 +17,10 @@ import (
"github.com/multiformats/go-multiaddr"
mn "github.com/multiformats/go-multiaddr/net"
"github.com/iden3/go-iden3-crypto/poseidon"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
func GetGRPCClient() (*grpc.ClientConn, error) {
@ -46,3 +58,163 @@ func GetGRPCClient() (*grpc.ClientConn, error) {
),
)
}
// CoinData represents a combined structure for coin information,
// including frame and address data.
type CoinData struct {
Amount string
Coin *protobufs.Coin
FrameNumber uint64
Address []byte
Timestamp string
}
func GetAccountCoins(includeMetadata bool) ([]CoinData, error) {
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
addr, err := poseidon.HashBytes([]byte(peerId))
if err != nil {
panic(err)
}
addrBytes := addr.FillBytes(make([]byte, 32))
resp, err := client.GetTokensByAccount(
context.Background(),
&protobufs.GetTokensByAccountRequest{
Address: addrBytes,
IncludeMetadata: includeMetadata,
},
)
if err != nil {
panic(err)
}
if len(resp.Coins) != len(resp.FrameNumbers) {
return nil, errors.New("invalid response from RPC")
}
pub, err := privKey.GetPublic().Raw()
if err != nil {
panic(err)
}
altAddr, err := poseidon.HashBytes([]byte(pub))
if err != nil {
panic(err)
}
altAddrBytes := altAddr.FillBytes(make([]byte, 32))
resp2, err := client.GetTokensByAccount(
context.Background(),
&protobufs.GetTokensByAccountRequest{
Address: altAddrBytes,
IncludeMetadata: includeMetadata,
},
)
if err != nil {
panic(err)
}
if len(resp2.Coins) != len(resp2.FrameNumbers) {
return nil, errors.New("invalid response from RPC")
}
// Merge coin data from both responses into a unified list
mergedData := make([]CoinData, 0, len(resp.Coins)+len(resp2.Coins))
// Add data from first response (resp)
for i := 0; i < len(resp.Coins); i++ {
coin := resp.Coins[i]
amount := new(big.Int).SetBytes(coin.Amount)
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
r := new(big.Rat).SetFrac(amount, conversionFactor)
data := CoinData{
Amount: r.FloatString(12),
Coin: resp.Coins[i],
FrameNumber: resp.FrameNumbers[i],
Address: resp.Addresses[i],
}
if len(resp.Timestamps) > i {
t := time.UnixMilli(resp.Timestamps[i])
data.Timestamp = t.Format(time.RFC3339)
}
mergedData = append(mergedData, data)
}
// Add data from second response (resp2)
for i := 0; i < len(resp2.Coins); i++ {
coin := resp2.Coins[i]
amount := new(big.Int).SetBytes(coin.Amount)
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
r := new(big.Rat).SetFrac(amount, conversionFactor)
data := CoinData{
Amount: r.FloatString(12),
Coin: resp2.Coins[i],
FrameNumber: resp2.FrameNumbers[i],
Address: resp2.Addresses[i],
}
if len(resp2.Timestamps) > i {
t := time.UnixMilli(resp2.Timestamps[i])
data.Timestamp = t.Format(time.RFC3339)
}
mergedData = append(mergedData, data)
}
return mergedData, nil
}
// IsAccountCoin checks if the given coin address is owned by the account.
func IsAccountCoin(address []byte, coins []CoinData) bool {
if len(coins) == 0 {
return false
}
for _, coin := range coins {
if bytes.Equal(coin.Address, address) {
return true
}
}
return false
}
func PromptTokenForTransfer(coins []CoinData) (string, error) {
fmt.Println("Please select a coin to transfer:")
for i, coin := range coins {
fmt.Printf("%d. %s\n", i+1, coin.Amount)
}
var selectedCoinIndex int
fmt.Scanln(&selectedCoinIndex)
if selectedCoinIndex < 1 || selectedCoinIndex > len(coins) {
return "", errors.New("invalid coin index")
}
return hex.EncodeToString(coins[selectedCoinIndex-1].Address), nil
}
func CleanAddress(address string) ([]byte, error) {
address = strings.ReplaceAll(address, "0x", "")
address = strings.TrimSpace(address)
addressBytes, err := hex.DecodeString(address)
if err != nil {
return nil, err
}
return addressBytes, nil
}

View File

@ -11,4 +11,5 @@ var (
ClientConfigFile = string(ReleaseTypeQClient) + "-config.yaml"
ClientConfigPath = filepath.Join(ClientConfigDir, ClientConfigFile)
DefaultQClientSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeQClient))
ReservedDefaultConfigName = "default"
)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
@ -18,6 +19,7 @@ func CreateDefaultConfig() {
SignatureCheck: true,
PublicRpc: false,
CustomRpc: "",
AddressList: make(map[string]string),
})
sudoUser, err := GetCurrentSudoUser()
@ -40,6 +42,7 @@ func LoadClientConfig() (*ClientConfig, error) {
SignatureCheck: true,
PublicRpc: false,
CustomRpc: "",
AddressList: make(map[string]string),
}
if err := SaveClientConfig(config); err != nil {
return nil, err
@ -89,3 +92,49 @@ func GetConfigDir() string {
func IsClientConfigured() bool {
return FileExists(ClientConfigPath)
}
func GetAddressList() (map[string]string, error) {
config, err := LoadClientConfig()
if err != nil {
return nil, err
}
// Check if AddressList is nil, and initialize it if necessary
if config.AddressList == nil {
config.AddressList = make(map[string]string)
}
// Get list of configs in ConfigDir (excluding default)
configDir := GetConfigDir()
if configDir == "" {
configDir = filepath.Join(GetUserQuilibriumDir(), "configs")
}
files, err := os.ReadDir(configDir)
if err != nil {
return nil, fmt.Errorf("failed to read config directory: %w", err)
}
for _, file := range files {
if !file.IsDir() || file.Name() == ReservedDefaultConfigName {
continue
}
tempConfig, err := LoadNodeConfig(file.Name())
if err != nil {
continue // Skip files that can't be parsed
}
address, err := GetAccountFromNodeConfig(tempConfig)
if err != nil {
continue // Skip files that can't be parsed
}
// Extract address from filename or content if available
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
if _, ok := config.AddressList[name]; ok {
config.AddressList[name] = string(address)
}
}
return config.AddressList, nil
}

View File

@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/pkg/errors"
"github.com/libp2p/go-libp2p/core/crypto"
@ -116,7 +117,7 @@ func LoadDefaultNodeConfig() (*config.Config, error) {
func LoadNodeConfig(configDirectory string) (*config.Config, error) {
// if the provided config directory is "default", load the default config
if configDirectory == "default" {
if configDirectory == ReservedDefaultConfigName {
return LoadDefaultNodeConfig()
}
@ -187,7 +188,7 @@ func SetDefaultNodeConfig(configName string) error {
}
// Construct the default directory path
defaultDir := filepath.Join(NodeConfigDir, "default")
defaultDir := filepath.Join(NodeConfigDir, ReservedDefaultConfigName)
// Create the symlink
if err := CreateSymlink(sourceDir, defaultDir); err != nil {
@ -224,3 +225,13 @@ func CreateDefaultNodeConfig(name string) (*config.Config, error) {
return nodeConfig, nil
}
func GetAccountFromNodeConfig(config *config.Config) ([]byte, error) {
peerId := GetPeerIDFromConfig(config)
addr, err := poseidon.HashBytes([]byte(peerId))
if err != nil {
panic(err)
}
return addr.FillBytes(make([]byte, 32)), nil
}

View File

@ -1,12 +1,13 @@
package utils
type ClientConfig struct {
DataDir string `yaml:"dataDir"`
SymlinkPath string `yaml:"symlinkPath"`
SignatureCheck bool `yaml:"signatureCheck"`
PublicRpc bool `yaml:"publicRpc"`
CustomRpc string `yaml:"customRpc"`
NodeSymlinkName string `yaml:"nodeSymlinkName"`
DataDir string `yaml:"dataDir"`
SymlinkPath string `yaml:"symlinkPath"`
SignatureCheck bool `yaml:"signatureCheck"`
PublicRpc bool `yaml:"publicRpc"`
CustomRpc string `yaml:"customRpc"`
NodeSymlinkName string `yaml:"nodeSymlinkName"`
AddressList map[string]string `yaml:"addresses"`
}
type NodeConfig struct {