From e7d3bfddcf3f457c4efdca1283da03eb0a4cf39a Mon Sep 17 00:00:00 2001 From: Brandon Chatham Date: Tue, 10 Dec 2024 16:03:35 -0800 Subject: [PATCH] Adding admin read commands with unit tests. --- go.mod | 2 +- go.sum | 2 + pkg/user/admin/admin.go | 8 +- pkg/user/admin/is_admin.go | 128 +++++++++++++++++++++++++-- pkg/user/admin/is_admin_test.go | 116 ++++++++++++++++++++++++ pkg/user/admin/is_pending.go | 124 ++++++++++++++++++++++++-- pkg/user/admin/is_pending_test.go | 116 ++++++++++++++++++++++++ pkg/user/admin/list.go | 128 +++++++++++++++++++++++++-- pkg/user/admin/list_pending.go | 131 ++++++++++++++++++++++++++-- pkg/user/admin/list_pending_test.go | 99 +++++++++++++++++++++ pkg/user/admin/list_test.go | 113 ++++++++++++++++++++++++ pkg/user/admin/types.go | 44 ++++++++++ pkg/user/appointee/list.go | 2 +- 13 files changed, 983 insertions(+), 30 deletions(-) create mode 100644 pkg/user/admin/is_admin_test.go create mode 100644 pkg/user/admin/is_pending_test.go create mode 100644 pkg/user/admin/list_pending_test.go create mode 100644 pkg/user/admin/list_test.go create mode 100644 pkg/user/admin/types.go diff --git a/go.mod b/go.mod index 03ae0e0..ef4b48b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Layr-Labs/eigenlayer-contracts v0.3.2-mainnet-rewards github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12 github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e - github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210222107-c2ed40624db7 + github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210234612-fdae59339a81 github.com/blang/semver/v4 v4.0.0 github.com/consensys/gnark-crypto v0.12.1 github.com/ethereum/go-ethereum v1.14.5 diff --git a/go.sum b/go.sum index c97cf52..dd83ec1 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210204558-54685ff8f493 h1:9HsmuJ github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210204558-54685ff8f493/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210222107-c2ed40624db7 h1:1kehcGgMyVloGzrd36CSibYz+fC2BkKV0fqeYCpovIQ= github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210222107-c2ed40624db7/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210234612-fdae59339a81 h1:max9ka+a5hx9/i/mbH1Y9GToXOCEtfsrt1BX02CAdYA= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241210234612-fdae59339a81/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= diff --git a/pkg/user/admin/admin.go b/pkg/user/admin/admin.go index 653dbc8..a90ef86 100644 --- a/pkg/user/admin/admin.go +++ b/pkg/user/admin/admin.go @@ -21,10 +21,10 @@ func AdminCmd() *cli.Command { Subcommands: []*cli.Command{ AcceptCmd(), AddPendingCmd(), - IsAdminCmd(), - IsPendingCmd(), - ListCmd(), - ListPendingCmd(), + IsAdminCmd(generateIsAdminReader), + IsPendingCmd(generateIsPendingAdminReader), + ListCmd(generateListAdminsReader), + ListPendingCmd(generateListPendingAdminsReader), RemoveCmd(), RemovePendingCmd(), }, diff --git a/pkg/user/admin/is_admin.go b/pkg/user/admin/is_admin.go index 6bd2d38..35bedf7 100644 --- a/pkg/user/admin/is_admin.go +++ b/pkg/user/admin/is_admin.go @@ -1,26 +1,138 @@ package admin import ( + "context" + "fmt" + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" ) -func IsAdminCmd() *cli.Command { - isAdmin := &cli.Command{ +type IsAdminReader interface { + IsAdmin( + ctx context.Context, + accountAddress gethcommon.Address, + pendingAdminAddress gethcommon.Address, + ) (bool, error) +} + +func IsAdminCmd(readerGenerator func(logging.Logger, *isAdminConfig) (IsAdminReader, error)) *cli.Command { + cmd := &cli.Command{ Name: "is-admin", Usage: "user admin is-admin --account-address --caller-address ", UsageText: "Checks if a user is an admin.", Description: ` Checks if a user is an admin. `, - After: telemetry.AfterRunAction(), - Flags: []cli.Flag{ - &flags.VerboseFlag, - &AccountAddressFlag, - &CallerAddressFlag, + Action: func(c *cli.Context) error { + return isAdmin(c, readerGenerator) }, + After: telemetry.AfterRunAction(), + Flags: IsAdminFlags(), + } + + return cmd +} + +func isAdmin(cliCtx *cli.Context, generator func(logging.Logger, *isAdminConfig) (IsAdminReader, error)) error { + ctx := cliCtx.Context + logger := common.GetLogger(cliCtx) + + config, err := readAndValidateIsAdminConfig(cliCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate user can call config", err) + } + cliCtx.App.Metadata["network"] = config.ChainID.String() + elReader, err := generator(logger, config) + if err != nil { + return err + } + + result, err := elReader.IsAdmin(ctx, config.AccountAddress, config.AdminAddress) + if err != nil { + return err } + fmt.Printf("IsAdmin Result: %v\n", result) + return nil +} + +func readAndValidateIsAdminConfig(cliContext *cli.Context, logger logging.Logger) (*isAdminConfig, error) { + accountAddress := gethcommon.HexToAddress(cliContext.String(AccountAddressFlag.Name)) + adminAddress := gethcommon.HexToAddress(cliContext.String(AdminAddressFlag.Name)) + ethRpcUrl := cliContext.String(flags.ETHRpcUrlFlag.Name) + network := cliContext.String(flags.NetworkFlag.Name) + environment := cliContext.String(flags.EnvironmentFlag.Name) + if environment == "" { + environment = common.GetEnvFromNetwork(network) + } + + chainID := utils.NetworkNameToChainId(network) + permissionManagerAddress := cliContext.String(PermissionControllerAddressFlag.Name) + + var err error + if common.IsEmptyString(permissionManagerAddress) { + permissionManagerAddress, err = common.GetPermissionManagerAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + + logger.Debugf( + "Env: %s, network: %s, chain ID: %s, PermissionManager address: %s", + environment, + network, + chainID, + permissionManagerAddress, + ) - return isAdmin + return &isAdminConfig{ + Network: network, + RPCUrl: ethRpcUrl, + AccountAddress: accountAddress, + AdminAddress: adminAddress, + PermissionManagerAddress: gethcommon.HexToAddress(permissionManagerAddress), + ChainID: chainID, + Environment: environment, + }, nil +} + +func generateIsAdminReader(logger logging.Logger, config *isAdminConfig) (IsAdminReader, error) { + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return nil, eigenSdkUtils.WrapError("failed to create new eth client", err) + } + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + PermissionsControllerAddress: config.PermissionManagerAddress, + }, + ethClient, + logger, + ) + return elReader, err +} + +func IsAdminFlags() []cli.Flag { + cmdFlags := []cli.Flag{ + &flags.VerboseFlag, + &AccountAddressFlag, + &CallerAddressFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &PermissionControllerAddressFlag, + &flags.NetworkFlag, + &flags.EnvironmentFlag, + &flags.ETHRpcUrlFlag, + } + sort.Sort(cli.FlagsByName(cmdFlags)) + return cmdFlags } diff --git a/pkg/user/admin/is_admin_test.go b/pkg/user/admin/is_admin_test.go new file mode 100644 index 0000000..f118fd6 --- /dev/null +++ b/pkg/user/admin/is_admin_test.go @@ -0,0 +1,116 @@ +package admin + +import ( + "context" + "errors" + "testing" + + "github.com/Layr-Labs/eigensdk-go/logging" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +type mockIsAdminReader struct { + isAdminFunc func(ctx context.Context, accountAddress gethcommon.Address, adminAddress gethcommon.Address) (bool, error) +} + +func (m *mockIsAdminReader) IsAdmin( + ctx context.Context, + accountAddress gethcommon.Address, + adminAddress gethcommon.Address, +) (bool, error) { + return m.isAdminFunc(ctx, accountAddress, adminAddress) +} + +func generateMockIsAdminReader(result bool, err error) func(logging.Logger, *isAdminConfig) (IsAdminReader, error) { + return func(logger logging.Logger, config *isAdminConfig) (IsAdminReader, error) { + return &mockIsAdminReader{ + isAdminFunc: func(ctx context.Context, accountAddress gethcommon.Address, adminAddress gethcommon.Address) (bool, error) { + return result, err + }, + }, nil + } +} + +func TestIsAdminCmd_Success(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsAdminCmd(generateMockIsAdminReader(true, nil)), + } + + args := []string{ + "TestIsAdminCmd_Success", + "is-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--caller-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestIsAdminCmd_NotAdmin(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsAdminCmd(generateMockIsAdminReader(false, nil)), + } + + args := []string{ + "TestIsAdminCmd_NotAdmin", + "is-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--caller-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestIsAdminCmd_GeneratorError(t *testing.T) { + expectedError := "failed to create admin reader" + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsAdminCmd(func(logger logging.Logger, config *isAdminConfig) (IsAdminReader, error) { + return nil, errors.New(expectedError) + }), + } + + args := []string{ + "TestIsAdminCmd_GeneratorError", + "is-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--caller-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} + +func TestIsAdminCmd_IsAdminError(t *testing.T) { + expectedError := "error checking admin status" + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsAdminCmd(generateMockIsAdminReader(false, errors.New(expectedError))), + } + + args := []string{ + "TestIsAdminCmd_IsAdminError", + "is-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--caller-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} diff --git a/pkg/user/admin/is_pending.go b/pkg/user/admin/is_pending.go index 7266113..1773acb 100644 --- a/pkg/user/admin/is_pending.go +++ b/pkg/user/admin/is_pending.go @@ -1,12 +1,32 @@ package admin import ( + "context" + "fmt" + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" ) -func IsPendingCmd() *cli.Command { +type IsPendingAdminReader interface { + IsPendingAdmin( + ctx context.Context, + accountAddress gethcommon.Address, + pendingAdminAddress gethcommon.Address, + ) (bool, error) +} + +func IsPendingCmd(readerGenerator func(logging.Logger, *isPendingAdminConfig) (IsPendingAdminReader, error)) *cli.Command { isPendingCmd := &cli.Command{ Name: "is-pending-admin", Usage: "user admin is-pending-admin --account-address --pending-admin-address ", @@ -14,13 +34,105 @@ func IsPendingCmd() *cli.Command { Description: ` Checks if a user is pending acceptance to admin. `, - After: telemetry.AfterRunAction(), - Flags: []cli.Flag{ - &flags.VerboseFlag, - &AccountAddressFlag, - &PendingAdminAddressFlag, + Action: func(c *cli.Context) error { + return isPendingAdmin(c, readerGenerator) }, + After: telemetry.AfterRunAction(), + Flags: isPendingAdminFlags(), } return isPendingCmd } + +func isPendingAdmin(cliCtx *cli.Context, generator func(logging.Logger, *isPendingAdminConfig) (IsPendingAdminReader, error)) error { + ctx := cliCtx.Context + logger := common.GetLogger(cliCtx) + + config, err := readAndValidateIsPendingAdminConfig(cliCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate user can call config", err) + } + cliCtx.App.Metadata["network"] = config.ChainID.String() + elReader, err := generator(logger, config) + if err != nil { + return err + } + + result, err := elReader.IsPendingAdmin(ctx, config.AccountAddress, config.PendingAdminAddress) + if err != nil { + return err + } + fmt.Printf("IsPendingAdmin Result: %v\n", result) + return nil +} + +func readAndValidateIsPendingAdminConfig(cliContext *cli.Context, logger logging.Logger) (*isPendingAdminConfig, error) { + accountAddress := gethcommon.HexToAddress(cliContext.String(AccountAddressFlag.Name)) + pendingAdminAddress := gethcommon.HexToAddress(cliContext.String(PendingAdminAddressFlag.Name)) + ethRpcUrl := cliContext.String(flags.ETHRpcUrlFlag.Name) + network := cliContext.String(flags.NetworkFlag.Name) + environment := cliContext.String(flags.EnvironmentFlag.Name) + if environment == "" { + environment = common.GetEnvFromNetwork(network) + } + + chainID := utils.NetworkNameToChainId(network) + permissionManagerAddress := cliContext.String(PermissionControllerAddressFlag.Name) + + var err error + if common.IsEmptyString(permissionManagerAddress) { + permissionManagerAddress, err = common.GetPermissionManagerAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + + logger.Debugf( + "Env: %s, network: %s, chain ID: %s, PermissionManager address: %s", + environment, + network, + chainID, + permissionManagerAddress, + ) + + return &isPendingAdminConfig{ + Network: network, + RPCUrl: ethRpcUrl, + AccountAddress: accountAddress, + PendingAdminAddress: pendingAdminAddress, + PermissionManagerAddress: gethcommon.HexToAddress(permissionManagerAddress), + ChainID: chainID, + Environment: environment, + }, nil +} + +func generateIsPendingAdminReader(logger logging.Logger, config *isPendingAdminConfig) (IsPendingAdminReader, error) { + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return nil, eigenSdkUtils.WrapError("failed to create new eth client", err) + } + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + PermissionsControllerAddress: config.PermissionManagerAddress, + }, + ethClient, + logger, + ) + return elReader, err +} + +func isPendingAdminFlags() []cli.Flag { + cmdFlags := []cli.Flag{ + &flags.VerboseFlag, + &AccountAddressFlag, + &PendingAdminAddressFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &PermissionControllerAddressFlag, + &flags.NetworkFlag, + &flags.EnvironmentFlag, + &flags.ETHRpcUrlFlag, + } + sort.Sort(cli.FlagsByName(cmdFlags)) + return cmdFlags +} diff --git a/pkg/user/admin/is_pending_test.go b/pkg/user/admin/is_pending_test.go new file mode 100644 index 0000000..18891f9 --- /dev/null +++ b/pkg/user/admin/is_pending_test.go @@ -0,0 +1,116 @@ +package admin + +import ( + "context" + "errors" + "testing" + + "github.com/Layr-Labs/eigensdk-go/logging" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +type mockIsPendingAdminReader struct { + isPendingAdminFunc func(ctx context.Context, accountAddress gethcommon.Address, pendingAdminAddress gethcommon.Address) (bool, error) +} + +func (m *mockIsPendingAdminReader) IsPendingAdmin( + ctx context.Context, + accountAddress gethcommon.Address, + pendingAdminAddress gethcommon.Address, +) (bool, error) { + return m.isPendingAdminFunc(ctx, accountAddress, pendingAdminAddress) +} + +func generateMockIsPendingAdminReader(result bool, err error) func(logging.Logger, *isPendingAdminConfig) (IsPendingAdminReader, error) { + return func(logger logging.Logger, config *isPendingAdminConfig) (IsPendingAdminReader, error) { + return &mockIsPendingAdminReader{ + isPendingAdminFunc: func(ctx context.Context, accountAddress gethcommon.Address, pendingAdminAddress gethcommon.Address) (bool, error) { + return result, err + }, + }, nil + } +} + +func TestIsPendingCmd_Success(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsPendingCmd(generateMockIsPendingAdminReader(true, nil)), + } + + args := []string{ + "TestIsPendingCmd_Success", + "is-pending-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--pending-admin-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestIsPendingCmd_NotPending(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsPendingCmd(generateMockIsPendingAdminReader(false, nil)), + } + + args := []string{ + "TestIsPendingCmd_NotPending", + "is-pending-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--pending-admin-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestIsPendingCmd_GeneratorError(t *testing.T) { + expectedError := "failed to create pending admin reader" + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsPendingCmd(func(logger logging.Logger, config *isPendingAdminConfig) (IsPendingAdminReader, error) { + return nil, errors.New(expectedError) + }), + } + + args := []string{ + "TestIsPendingCmd_GeneratorError", + "is-pending-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--pending-admin-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} + +func TestIsPendingCmd_IsPendingAdminError(t *testing.T) { + expectedError := "error checking pending admin status" + app := cli.NewApp() + app.Commands = []*cli.Command{ + IsPendingCmd(generateMockIsPendingAdminReader(false, errors.New(expectedError))), + } + + args := []string{ + "TestIsPendingCmd_IsPendingAdminError", + "is-pending-admin", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--pending-admin-address", "0x1234567890abcdef1234567890abcdef12345678", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} diff --git a/pkg/user/admin/list.go b/pkg/user/admin/list.go index b4fd204..403ac3d 100644 --- a/pkg/user/admin/list.go +++ b/pkg/user/admin/list.go @@ -1,12 +1,32 @@ package admin import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" ) -func ListCmd() *cli.Command { +type ListAdminsReader interface { + ListAdmins( + ctx context.Context, + userAddress gethcommon.Address, + ) ([]gethcommon.Address, error) +} + +func ListCmd(readerGenerator func(logging.Logger, *listAdminsConfig) (ListAdminsReader, error)) *cli.Command { listCmd := &cli.Command{ Name: "list-admins", Usage: "user admin list-admins --account-address ", @@ -14,12 +34,110 @@ func ListCmd() *cli.Command { Description: ` List all users who are admins. `, - After: telemetry.AfterRunAction(), - Flags: []cli.Flag{ - &flags.VerboseFlag, - &AccountAddressFlag, + Action: func(c *cli.Context) error { + return listAdmins(c, readerGenerator) }, + After: telemetry.AfterRunAction(), + Flags: listAdminFlags(), } return listCmd } + +func listAdmins(cliCtx *cli.Context, generator func(logging.Logger, *listAdminsConfig) (ListAdminsReader, error)) error { + ctx := cliCtx.Context + logger := common.GetLogger(cliCtx) + + config, err := readAndValidateListAdminsConfig(cliCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate user can call config", err) + } + cliCtx.App.Metadata["network"] = config.ChainID.String() + elReader, err := generator(logger, config) + if err != nil { + return err + } + + pendingAdmins, err := elReader.ListAdmins(ctx, config.AccountAddress) + if err != nil { + return err + } + printAdmins(config.AccountAddress, pendingAdmins) + return nil +} + +func printAdmins(account gethcommon.Address, admins []gethcommon.Address) { + fmt.Printf("Admins for Account: %s \n", account) + fmt.Println(strings.Repeat("=", 60)) + for _, admin := range admins { + fmt.Printf("%s \n", admin.String()) + } +} + +func readAndValidateListAdminsConfig(cliContext *cli.Context, logger logging.Logger) (*listAdminsConfig, error) { + accountAddress := gethcommon.HexToAddress(cliContext.String(AccountAddressFlag.Name)) + ethRpcUrl := cliContext.String(flags.ETHRpcUrlFlag.Name) + network := cliContext.String(flags.NetworkFlag.Name) + environment := cliContext.String(flags.EnvironmentFlag.Name) + if environment == "" { + environment = common.GetEnvFromNetwork(network) + } + + chainID := utils.NetworkNameToChainId(network) + permissionManagerAddress := cliContext.String(PermissionControllerAddressFlag.Name) + + var err error + if common.IsEmptyString(permissionManagerAddress) { + permissionManagerAddress, err = common.GetPermissionManagerAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + + logger.Debugf( + "Env: %s, network: %s, chain ID: %s, PermissionManager address: %s", + environment, + network, + chainID, + permissionManagerAddress, + ) + + return &listAdminsConfig{ + Network: network, + RPCUrl: ethRpcUrl, + AccountAddress: accountAddress, + PermissionManagerAddress: gethcommon.HexToAddress(permissionManagerAddress), + ChainID: chainID, + Environment: environment, + }, nil +} + +func generateListAdminsReader(logger logging.Logger, config *listAdminsConfig) (ListAdminsReader, error) { + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return nil, eigenSdkUtils.WrapError("failed to create new eth client", err) + } + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + PermissionsControllerAddress: config.PermissionManagerAddress, + }, + ethClient, + logger, + ) + return elReader, err +} + +func listAdminFlags() []cli.Flag { + cmdFlags := []cli.Flag{ + &flags.VerboseFlag, + &AccountAddressFlag, + &flags.OutputFileFlag, + &flags.OutputTypeFlag, + &PermissionControllerAddressFlag, + &flags.NetworkFlag, + &flags.EnvironmentFlag, + &flags.ETHRpcUrlFlag, + } + sort.Sort(cli.FlagsByName(cmdFlags)) + return cmdFlags +} diff --git a/pkg/user/admin/list_pending.go b/pkg/user/admin/list_pending.go index b12f0c6..d0145ff 100644 --- a/pkg/user/admin/list_pending.go +++ b/pkg/user/admin/list_pending.go @@ -1,12 +1,32 @@ package admin import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" ) -func ListPendingCmd() *cli.Command { +type ListPendingAdminsReader interface { + ListPendingAdmins( + ctx context.Context, + userAddress gethcommon.Address, + ) ([]gethcommon.Address, error) +} + +func ListPendingCmd(readerGenerator func(logging.Logger, *listPendingAdminsConfig) (ListPendingAdminsReader, error)) *cli.Command { listPendingCmd := &cli.Command{ Name: "list-pending-admins", Usage: "user admin list-pending-admins --account-address ", @@ -14,12 +34,113 @@ func ListPendingCmd() *cli.Command { Description: ` List all users who are pending admin acceptance. `, - After: telemetry.AfterRunAction(), - Flags: []cli.Flag{ - &flags.VerboseFlag, - &AccountAddressFlag, + Action: func(c *cli.Context) error { + return listPendingAdmins(c, readerGenerator) }, + After: telemetry.AfterRunAction(), + Flags: listPendingAdminsFlags(), } return listPendingCmd } + +func listPendingAdmins(cliCtx *cli.Context, generator func(logging.Logger, *listPendingAdminsConfig) (ListPendingAdminsReader, error)) error { + ctx := cliCtx.Context + logger := common.GetLogger(cliCtx) + + config, err := readAndValidateListPendingAdminsConfig(cliCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate user can call config", err) + } + cliCtx.App.Metadata["network"] = config.ChainID.String() + elReader, err := generator(logger, config) + if err != nil { + return err + } + + pendingAdmins, err := elReader.ListPendingAdmins(ctx, config.AccountAddress) + if err != nil { + return err + } + printPendingAdmins(config.AccountAddress, pendingAdmins) + return nil +} + +func printPendingAdmins(account gethcommon.Address, admins []gethcommon.Address) { + fmt.Printf("Pending Admins\n for Account: %s", account) + fmt.Println(strings.Repeat("=", 60)) + for _, admin := range admins { + fmt.Printf("%s \n", admin.String()) + } +} + +func readAndValidateListPendingAdminsConfig(cliContext *cli.Context, logger logging.Logger) (*listPendingAdminsConfig, error) { + accountAddress := gethcommon.HexToAddress(cliContext.String(AccountAddressFlag.Name)) + ethRpcUrl := cliContext.String(flags.ETHRpcUrlFlag.Name) + network := cliContext.String(flags.NetworkFlag.Name) + environment := cliContext.String(flags.EnvironmentFlag.Name) + if environment == "" { + environment = common.GetEnvFromNetwork(network) + } + + chainID := utils.NetworkNameToChainId(network) + permissionManagerAddress := cliContext.String(PermissionControllerAddressFlag.Name) + + var err error + if common.IsEmptyString(permissionManagerAddress) { + permissionManagerAddress, err = common.GetPermissionManagerAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + + logger.Debugf( + "Env: %s, network: %s, chain ID: %s, PermissionManager address: %s", + environment, + network, + chainID, + permissionManagerAddress, + ) + + return &listPendingAdminsConfig{ + Network: network, + RPCUrl: ethRpcUrl, + AccountAddress: accountAddress, + PermissionManagerAddress: gethcommon.HexToAddress(permissionManagerAddress), + ChainID: chainID, + Environment: environment, + }, nil +} + +func generateListPendingAdminsReader( + logger logging.Logger, + config *listPendingAdminsConfig, +) (ListPendingAdminsReader, error) { + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return nil, eigenSdkUtils.WrapError("failed to create new eth client", err) + } + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + PermissionsControllerAddress: config.PermissionManagerAddress, + }, + ethClient, + logger, + ) + return elReader, err +} + +func listPendingAdminsFlags() []cli.Flag { + cmdFlags := []cli.Flag{ + &flags.VerboseFlag, + &AccountAddressFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &PermissionControllerAddressFlag, + &flags.NetworkFlag, + &flags.EnvironmentFlag, + &flags.ETHRpcUrlFlag, + } + sort.Sort(cli.FlagsByName(cmdFlags)) + return cmdFlags +} diff --git a/pkg/user/admin/list_pending_test.go b/pkg/user/admin/list_pending_test.go new file mode 100644 index 0000000..7f9079a --- /dev/null +++ b/pkg/user/admin/list_pending_test.go @@ -0,0 +1,99 @@ +package admin + +import ( + "context" + "errors" + "testing" + + "github.com/Layr-Labs/eigensdk-go/logging" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + "github.com/urfave/cli/v2" +) + +type mockListPendingAdminsReader struct { + listPendingAdminsFunc func(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) +} + +func (m *mockListPendingAdminsReader) ListPendingAdmins(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) { + return m.listPendingAdminsFunc(ctx, userAddress) +} + +func generateMockListPendingAdminsReader(admins []gethcommon.Address, err error) func(logging.Logger, *listPendingAdminsConfig) (ListPendingAdminsReader, error) { + return func(logger logging.Logger, config *listPendingAdminsConfig) (ListPendingAdminsReader, error) { + return &mockListPendingAdminsReader{ + listPendingAdminsFunc: func(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) { + return admins, err + }, + }, nil + } +} + +func TestListPendingCmd_Success(t *testing.T) { + expectedAdmins := []gethcommon.Address{ + gethcommon.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), + gethcommon.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"), + } + + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListPendingCmd(generateMockListPendingAdminsReader(expectedAdmins, nil)), + } + + args := []string{ + "TestListPendingCmd_Success", + "list-pending-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + "--permission-controller-address", "0xe4dB7125ef7a9D99F809B6b7788f75c8D84d8455", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestListPendingCmd_GeneratorError(t *testing.T) { + expectedError := "failed to create pending admins reader" + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListPendingCmd(func(logger logging.Logger, config *listPendingAdminsConfig) (ListPendingAdminsReader, error) { + return nil, errors.New(expectedError) + }), + } + + args := []string{ + "TestListPendingCmd_GeneratorError", + "list-pending-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + "--permission-controller-address", "0xe4dB7125ef7a9D99F809B6b7788f75c8D84d8455", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} + +func TestListPendingCmd_ListPendingError(t *testing.T) { + expectedError := "failed to fetch pending admins" + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListPendingCmd(generateMockListPendingAdminsReader(nil, errors.New(expectedError))), + } + + args := []string{ + "TestListPendingCmd_ListPendingError", + "list-pending-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + "--permission-controller-address", "0xe4dB7125ef7a9D99F809B6b7788f75c8D84d8455", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} diff --git a/pkg/user/admin/list_test.go b/pkg/user/admin/list_test.go new file mode 100644 index 0000000..b023660 --- /dev/null +++ b/pkg/user/admin/list_test.go @@ -0,0 +1,113 @@ +package admin + +import ( + "context" + "errors" + "testing" + + "github.com/Layr-Labs/eigensdk-go/logging" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +type mockListAdminsReader struct { + listAdminsFunc func(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) +} + +func (m *mockListAdminsReader) ListAdmins(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) { + return m.listAdminsFunc(ctx, userAddress) +} + +func generateMockListAdminsReader(admins []gethcommon.Address, err error) func(logging.Logger, *listAdminsConfig) (ListAdminsReader, error) { + return func(logger logging.Logger, config *listAdminsConfig) (ListAdminsReader, error) { + return &mockListAdminsReader{ + listAdminsFunc: func(ctx context.Context, userAddress gethcommon.Address) ([]gethcommon.Address, error) { + return admins, err + }, + }, nil + } +} + +func TestListCmd_Success(t *testing.T) { + expectedAdmins := []gethcommon.Address{ + gethcommon.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), + gethcommon.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"), + } + + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListCmd(generateMockListAdminsReader(expectedAdmins, nil)), + } + + args := []string{ + "TestListCmd_Success", + "list-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestListCmd_NoAdmins(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListCmd(generateMockListAdminsReader([]gethcommon.Address{}, nil)), + } + + args := []string{ + "TestListCmd_NoAdmins", + "list-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.NoError(t, err) +} + +func TestListCmd_GeneratorError(t *testing.T) { + expectedError := "failed to create admin reader" + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListCmd(func(logger logging.Logger, config *listAdminsConfig) (ListAdminsReader, error) { + return nil, errors.New(expectedError) + }), + } + + args := []string{ + "TestListCmd_GeneratorError", + "list-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} + +func TestListCmd_ListAdminsError(t *testing.T) { + expectedError := "failed to fetch admins" + app := cli.NewApp() + app.Commands = []*cli.Command{ + ListCmd(generateMockListAdminsReader(nil, errors.New(expectedError))), + } + + args := []string{ + "TestListCmd_ListAdminsError", + "list-admins", + "--account-address", "0xabcdef1234567890abcdef1234567890abcdef12", + "--eth-rpc-url", "https://ethereum-holesky.publicnode.com/", + "--network", "holesky", + } + + err := app.Run(args) + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError) +} diff --git a/pkg/user/admin/types.go b/pkg/user/admin/types.go new file mode 100644 index 0000000..bc13e9f --- /dev/null +++ b/pkg/user/admin/types.go @@ -0,0 +1,44 @@ +package admin + +import ( + gethcommon "github.com/ethereum/go-ethereum/common" + "math/big" +) + +type listPendingAdminsConfig struct { + Network string + RPCUrl string + AccountAddress gethcommon.Address + PermissionManagerAddress gethcommon.Address + ChainID *big.Int + Environment string +} + +type listAdminsConfig struct { + Network string + RPCUrl string + AccountAddress gethcommon.Address + PermissionManagerAddress gethcommon.Address + ChainID *big.Int + Environment string +} + +type isPendingAdminConfig struct { + Network string + RPCUrl string + AccountAddress gethcommon.Address + PendingAdminAddress gethcommon.Address + PermissionManagerAddress gethcommon.Address + ChainID *big.Int + Environment string +} + +type isAdminConfig struct { + Network string + RPCUrl string + AccountAddress gethcommon.Address + AdminAddress gethcommon.Address + PermissionManagerAddress gethcommon.Address + ChainID *big.Int + Environment string +} diff --git a/pkg/user/appointee/list.go b/pkg/user/appointee/list.go index a627816..6377c3c 100644 --- a/pkg/user/appointee/list.go +++ b/pkg/user/appointee/list.go @@ -77,7 +77,7 @@ func printResults(config *listUsersConfig, users []gethcommon.Address) { string(config.Selector[:]), config.UserAddress, ) - fmt.Println(strings.Repeat("-", 80)) + fmt.Println(strings.Repeat("=", 80)) for _, user := range users { fmt.Printf("User Id: 0x%b\n", user)