Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] feat: CLI integration for notification services #299

Draft
wants to merge 4 commits into
base: feat/slashing
Choose a base branch
from

Conversation

gpabst
Copy link
Collaborator

@gpabst gpabst commented Mar 14, 2025

eigenlayer notifications - Subscribe and unsubscribe to EigenLayer events via the notification service

USAGE:
   eigenlayer notifications command [command options] 

COMMANDS:
   list-events  List all the events available to be subscribed to via notification service for a given AVS
   list-avs     List all the AVSs available to be subscribed to via notification service
   subscribe    Subscribe to an event via the notification service
   unsubscribe  Unsubscribe from an existging subscription via the notification service
   help, h      Shows a list of commands or help for one command

OPTIONS:
   --help, -h  show help

Samples with stub data generated by AI:

eigenlayer notifications list-events --avs-name eigenda
Available Events for AVS 'eigenda':
----------------
Event Name: DataStored
Contract Address: 0x6d50c205ca13f2b89d1021efd9eed37d10ea8bef
Ethereum Topic: 0x9c72852172521097ba7e1482e6b44b351323df0155f97f3e2772a0c3b17044a9
----------------
Event Name: DataRetrieved
Contract Address: 0x6d50c205ca13f2b89d1021efd9eed37d10ea8bef
Ethereum Topic: 0x8c0f34a9a45f07d3c8a7c56f95009c407c44f78f35ee7dba849edd9a08e224e6
eigenlayer notifications list-avs                            
Available AVS Services:
----------------------
Name: eigenda
Display Name: EigenDA
Description: Decentralized data availability layer for rollups
Status: active
----------------------
Name: eigenlayer-operator
Display Name: EigenLayer Operator
Description: Core EigenLayer operator service
Status: active
----------------------
Name: eigenlayer-node
Display Name: EigenLayer Node
Description: EigenLayer node service for validators
Status: active
----------------------
Name: restaking-service
Display Name: EigenLayer Restaking
Description: Restaking service for ETH and LSTs
Status: active
----------------------
Name: avs-marketplace
Display Name: AVS Marketplace
Description: Marketplace for AVS services and operators
Status: planned
./eigenlayer notifications subscribe --avs-name eigenda --event-name DataStored --operator-id "operator12345" --delivery-method "email" --delivery-details "[email protected]"

Successfully subscribed to DataStored events for AVS 'eigenda'

@nelsonijih nelsonijih marked this pull request as ready for review March 14, 2025 14:05
@nelsonijih
Copy link
Contributor

would be great to see sample out of the commands.

@gpabst gpabst changed the title feat: CLI integration for notification services [DRAFT]feat: CLI integration for notification services Mar 14, 2025
@gpabst gpabst changed the title [DRAFT]feat: CLI integration for notification services [DRAFT] feat: CLI integration for notification services Mar 14, 2025
@gpabst gpabst marked this pull request as draft March 14, 2025 14:12
var (
AvsNameFlag = cli.StringFlag{
Name: "avs-name",
Aliases: []string{"k"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alias names seem off on a few of these flags.

"github.com/urfave/cli/v2"
)

func NotificationsCmd(p utils.Prompter) *cli.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the BaseCommand pattern for new commands?

@@ -39,6 +39,7 @@ func main() {
return nil
}

app.Commands = append(app.Commands, pkg.NotificationsCmd(prompter))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: idiomatic go is New.. for factory methods.

Message string `json:"message"`
}

const baseURL = "http://localhost:3000/api"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would suggest passing this as a variable for better testing and parameterization.

Comment on lines 25 to 28
type ErrorResponse struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these to a shared location? It's going to become a common theme.

Comment on lines 23 to 33
var DeliveryMethodFlag = cli.StringFlag{
Name: "delivery-method",
Usage: "Method of delivery for notifications (email, webhook, or telegram)",
Required: true,
}

var DeliveryDetailsFlag = cli.StringFlag{
Name: "delivery-details",
Usage: "Details for the delivery method (email address, webhook URL, or telegram chat ID)",
Required: true,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not have this in the flag file?

}

func validateDeliveryMethod(method string) error {
validMethods := map[string]bool{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: every time you call this method you create a new map that never changes. You can turn this into a var. Once you do this you don't really need the method unless you are trying to standardize the error message. Any caller who uses the method anyways similarly has to have an if err { ... } clause.

}

// Accept both 200 and 201 as success codes
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference from your service perspective in these two cases?

return nil
}

func SubscribeEventsCmd() *cli.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other long method, this should be broken down. There are very different, separated tasks here between input validation & parsing, http request dispatch & processing and response handling.

I think it's important for us trend away from monolithic commands and not towards them.


const baseURL = "http://localhost:3000/api"

func ListEventsCmd() *cli.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command is a single function, 60 lines long. I think it's worth discussing why there seems to be a preference for large, monolithic commands, rather than following the principle of code segments doing one thing, and one thing clearly.

func NotificationsCmd(p utils.Prompter) *cli.Command {
var notificationsCmd = &cli.Command{
Name: "notifications",
Usage: "Subscribe and unsubscribe to EigenLayer events via the notification service",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notification Service?

Usage: "List all the AVSs available to be subscribed to via notification service",
UsageText: "list-avs",
Description: `
This command provides a listing of all AVS services with events available to be subscribed via the notification service.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command provides -> Provides

'This command' is implied by this being CLI help

@gpabst
Copy link
Collaborator Author

gpabst commented Mar 17, 2025

Thanks for the feedback on the draft, will work through it. I have pushed some refactoring already that should resolve some of the code issues.

Comment on lines +61 to +63
if !isSuccessStatusCode(resp.StatusCode) {
return handleErrorResponse(resp.StatusCode, body)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved into the HTTP code path, no? Same for the other place it's called.

return APIBaseURL
}

// doHTTPRequest performs the HTTP request and processes the response
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really utils or is it an http client? Seems like something we want to generalize and use in other places, like ListOperatorApplications for RMS.


// getHTTPClient returns the shared HTTP client instance
func getHTTPClient() *http.Client {
httpClientOnce.Do(func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you turn this into a normal object you don't need to enforce only-once instantiation.

)

// defaultTimeout is the default timeout for HTTP requests
const defaultTimeout = 10 * time.Second
Copy link
Contributor

@bdchatham bdchatham Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to have a default, but this should also be parameterized for overriding and testing.

return makeRequest(ctx, config)
}

// makeDeleteRequest makes a DELETE request to the specified URL with proper headers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is it meaningful to have comments like this?

}

// AvailableAvsResponseDto represents the API response for available AVS services.
type AvailableAvsResponseDto struct {
Copy link
Contributor

@bdchatham bdchatham Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service question: how does available AVS list get populated server side?

SubscriptionIDFlag = cli.StringFlag{
Name: "subscription-id",
Aliases: []string{"id"},
Required: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your validation, this is required.

AvsNameFlag = cli.StringFlag{
Name: "avs-name",
Aliases: []string{"avs"},
Required: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your validation, this is required.

Comment on lines +22 to +51
func validateSubscribeParams(cliCtx *cli.Context) (*SubscriptionParams, error) {
params := &SubscriptionParams{}

params.AvsName = cliCtx.String("avs-name")
if params.AvsName == "" {
return nil, fmt.Errorf("avs-name is required")
}

params.EventName = cliCtx.String("event-name")
if params.EventName == "" {
return nil, fmt.Errorf("event-name is required")
}

params.OperatorID = cliCtx.String("operator-id")
if params.OperatorID == "" {
return nil, fmt.Errorf("operator-id is required")
}

params.DeliveryMethod = cliCtx.String("delivery-method")
if err := validateDeliveryMethod(params.DeliveryMethod); err != nil {
return nil, err
}

params.DeliveryDetails = cliCtx.String("delivery-details")
if params.DeliveryDetails == "" {
return nil, fmt.Errorf("delivery-details is required")
}

return params, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear to me why we need this if you are marking these flags as required with urfacecli directly. It will automatically validate them, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants