diff --git a/client/cmd/config/alias.go b/client/cmd/config/alias.go new file mode 100644 index 0000000..63bc248 --- /dev/null +++ b/client/cmd/config/alias.go @@ -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) +} diff --git a/client/cmd/config/config.go b/client/cmd/config/config.go index 88da1c7..76e3c47 100644 --- a/client/cmd/config/config.go +++ b/client/cmd/config/config.go @@ -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) } diff --git a/client/cmd/crossMint.go b/client/cmd/crossMint.go index c091dfd..5f75cc6 100644 --- a/client/cmd/crossMint.go +++ b/client/cmd/crossMint.go @@ -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") } diff --git a/client/cmd/node/info.go b/client/cmd/node/info.go index 528d028..ce01eb3 100644 --- a/client/cmd/node/info.go +++ b/client/cmd/node/info.go @@ -27,7 +27,7 @@ Examples: if len(args) > 0 { NodeGetInfo(args[0]) } else { - NodeGetInfo("default") + NodeGetInfo(utils.ReservedDefaultConfigName) } }, } diff --git a/client/cmd/node/nodeconfig/create.go b/client/cmd/node/nodeconfig/create.go index 6cf8c14..e2f3e8c 100644 --- a/client/cmd/node/nodeconfig/create.go +++ b/client/cmd/node/nodeconfig/create.go @@ -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") } diff --git a/client/cmd/node/nodeconfig/switch.go b/client/cmd/node/nodeconfig/switch.go index cc5e252..4a67e5f 100644 --- a/client/cmd/node/nodeconfig/switch.go +++ b/client/cmd/node/nodeconfig/switch.go @@ -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) } diff --git a/client/cmd/node/nodeconfig/utils.go b/client/cmd/node/nodeconfig/utils.go index 5acc2c9..2ba2532 100644 --- a/client/cmd/node/nodeconfig/utils.go +++ b/client/cmd/node/nodeconfig/utils.go @@ -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()) } } diff --git a/client/cmd/token/account.go b/client/cmd/token/account.go index 4cf59c3..8f4b7e1 100644 --- a/client/cmd/token/account.go +++ b/client/cmd/token/account.go @@ -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) }, } diff --git a/client/cmd/token/utils.go b/client/cmd/token/utils.go index 59f1eb1..f0b7bb0 100644 --- a/client/cmd/token/utils.go +++ b/client/cmd/token/utils.go @@ -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 +} diff --git a/client/utils/client.go b/client/utils/client.go index 108dcc5..c4716b0 100644 --- a/client/utils/client.go +++ b/client/utils/client.go @@ -11,4 +11,5 @@ var ( ClientConfigFile = string(ReleaseTypeQClient) + "-config.yaml" ClientConfigPath = filepath.Join(ClientConfigDir, ClientConfigFile) DefaultQClientSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeQClient)) + ReservedDefaultConfigName = "default" ) diff --git a/client/utils/clientConfig.go b/client/utils/clientConfig.go index 8cd846c..23d4b89 100644 --- a/client/utils/clientConfig.go +++ b/client/utils/clientConfig.go @@ -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 +} diff --git a/client/utils/node.go b/client/utils/node.go index 407b003..181ca50 100644 --- a/client/utils/node.go +++ b/client/utils/node.go @@ -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 +} diff --git a/client/utils/types.go b/client/utils/types.go index f9f8230..6387b72 100644 --- a/client/utils/types.go +++ b/client/utils/types.go @@ -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 {