diff --git a/README.md b/README.md index d60f12a..b91643b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,30 @@ All scenarios require: - `--privkey` - Private key for the sending wallet - `--rpchost` - RPC endpoint(s) to send transactions to +### RPC Host Configuration + +RPC hosts support additional configuration parameters through URL prefixes: + +- `headers(key:value|key2:value2)` - Sets custom HTTP headers +- `group(name)` - Assigns the client to a named group (can be used multiple times) +- `group(name1,name2,name3)` - Assigns the client to multiple groups (comma-separated) +- `name(custom_name)` - Sets a custom display name override for the client + +**Examples:** +```bash +# Basic RPC endpoint +--rpchost="http://localhost:8545" + +# With custom headers and groups +--rpchost="headers(Authorization:Bearer token|User-Agent:MyApp)group(mainnet)group(primary)http://localhost:8545" + +# With custom name and multiple groups +--rpchost="group(mainnet,primary,backup)name(MainNet Primary)http://localhost:8545" + +# Full configuration example +--rpchost="headers(Authorization:Bearer token)group(mainnet)name(My Custom Node)http://localhost:8545" +``` + ## Scenarios Spamoor provides multiple scenarios for different transaction types: @@ -75,8 +99,14 @@ The web interface runs on `http://localhost:8080` by default and provides: - Start/pause/delete functionality ### API -The daemon exposes a REST API for programmatic control. -See the API Documentation in the spamoor web interface for details. +The daemon exposes a REST API for programmatic control, including: + +- **Client Management**: Get client information, update client groups, enable/disable clients +- **Client Name Override**: Set custom display names for RPC clients via `PUT /api/client/{index}/name` +- **Spammer Control**: Create, start, pause, and delete spammers +- **Export/Import**: Export and import spammer configurations + +See the API Documentation in the spamoor web interface for complete details. ### Export/Import Functionality Spamoor supports exporting and importing spammer configurations as YAML files: diff --git a/spamoor/client.go b/spamoor/client.go index b28fdd4..5e7d61a 100644 --- a/spamoor/client.go +++ b/spamoor/client.go @@ -28,6 +28,7 @@ type Client struct { clientGroups []string enabled bool + nameOverride string gasSuggestionMutex sync.Mutex lastGasSuggestion time.Time @@ -47,12 +48,14 @@ type Client struct { // - headers(key:value|key2:value2) - sets custom HTTP headers // - group(name) - assigns the client to a named group (can be used multiple times) // - group(name1,name2,name3) - assigns the client to multiple groups (comma-separated) +// - name(custom_name) - sets a custom display name override // -// Example: "headers(Authorization:Bearer token|User-Agent:MyApp)group(mainnet)group(primary)http://localhost:8545" -// Example: "group(mainnet,primary,backup)http://localhost:8545" +// Example: "headers(Authorization:Bearer token|User-Agent:MyApp)group(mainnet)group(primary)name(My Custom Node)http://localhost:8545" +// Example: "group(mainnet,primary,backup)name(MainNet Primary)http://localhost:8545" func NewClient(rpchost string) (*Client, error) { headers := map[string]string{} clientGroups := []string{"default"} + nameOverride := "" for { if strings.HasPrefix(rpchost, "headers(") { @@ -87,6 +90,10 @@ func NewClient(rpchost string) (*Client, error) { } } } + } else if strings.HasPrefix(rpchost, "name(") { + nameEnd := strings.Index(rpchost, ")") + nameOverride = rpchost[5:nameEnd] + rpchost = rpchost[nameEnd+1:] } else { break } @@ -109,12 +116,17 @@ func NewClient(rpchost string) (*Client, error) { logger: logrus.WithField("rpc", rpchost), clientGroups: clientGroups, enabled: true, + nameOverride: nameOverride, }, nil } // GetName returns a shortened name for the client derived from the RPC host URL, // removing common suffixes like ".ethpandaops.io". +// If a name override is set, it returns the override instead. func (client *Client) GetName() string { + if client.nameOverride != "" { + return client.nameOverride + } url, _ := url.Parse(client.rpchost) name := strings.TrimSuffix(url.Host, ".ethpandaops.io") return name @@ -221,6 +233,17 @@ func (client *Client) SetEnabled(enabled bool) { client.enabled = enabled } +// GetNameOverride returns the name override for the client. +func (client *Client) GetNameOverride() string { + return client.nameOverride +} + +// SetNameOverride sets a custom name override for the client. +// If set, this name will be used instead of the auto-generated name from the RPC host. +func (client *Client) SetNameOverride(name string) { + client.nameOverride = name +} + func (client *Client) getContext(ctx context.Context) (context.Context, context.CancelFunc) { if client.Timeout > 0 { return context.WithTimeout(ctx, client.Timeout) diff --git a/spamoor/spamoor-daemon b/spamoor/spamoor-daemon new file mode 100755 index 0000000..de0e4bb Binary files /dev/null and b/spamoor/spamoor-daemon differ diff --git a/webui/handlers/api/api.go b/webui/handlers/api/api.go index 6f8d750..0063d9f 100644 --- a/webui/handlers/api/api.go +++ b/webui/handlers/api/api.go @@ -545,15 +545,16 @@ func (ah *APIHandler) StreamSpammerLogs(w http.ResponseWriter, r *http.Request) // ClientEntry represents a client in the API response type ClientEntry struct { - Index int `json:"index"` - Name string `json:"name"` - Group string `json:"group"` // First group for backward compatibility - Groups []string `json:"groups"` // All groups - Version string `json:"version"` - BlockHeight uint64 `json:"block_height"` - IsReady bool `json:"ready"` - RpcHost string `json:"rpc_host"` - Enabled bool `json:"enabled"` + Index int `json:"index"` + Name string `json:"name"` + Group string `json:"group"` // First group for backward compatibility + Groups []string `json:"groups"` // All groups + Version string `json:"version"` + BlockHeight uint64 `json:"block_height"` + IsReady bool `json:"ready"` + RpcHost string `json:"rpc_host"` + Enabled bool `json:"enabled"` + NameOverride string `json:"name_override,omitempty"` } // UpdateClientGroupRequest represents the request body for updating a client group @@ -567,6 +568,11 @@ type UpdateClientEnabledRequest struct { Enabled bool `json:"enabled"` } +// UpdateClientNameRequest represents the request body for updating a client's name override +type UpdateClientNameRequest struct { + NameOverride string `json:"name_override"` +} + // GetClients godoc // @Id getClients // @Summary Get all clients @@ -591,15 +597,16 @@ func (ah *APIHandler) GetClients(w http.ResponseWriter, r *http.Request) { } response[i] = ClientEntry{ - Index: i, - Name: client.GetName(), - Group: client.GetClientGroup(), - Groups: client.GetClientGroups(), - Version: version, - BlockHeight: blockHeight, - IsReady: slices.Contains(goodClients, client), - RpcHost: client.GetRPCHost(), - Enabled: client.IsEnabled(), + Index: i, + Name: client.GetName(), + Group: client.GetClientGroup(), + Groups: client.GetClientGroups(), + Version: version, + BlockHeight: blockHeight, + IsReady: slices.Contains(goodClients, client), + RpcHost: client.GetRPCHost(), + Enabled: client.IsEnabled(), + NameOverride: client.GetNameOverride(), } } @@ -692,6 +699,44 @@ func (ah *APIHandler) UpdateClientEnabled(w http.ResponseWriter, r *http.Request w.WriteHeader(http.StatusOK) } +// UpdateClientName godoc +// @Id updateClientName +// @Summary Update client name override +// @Tags Client +// @Description Updates the name override for a specific client +// @Accept json +// @Param index path int true "Client index" +// @Param request body UpdateClientNameRequest true "New name override" +// @Success 200 {object} Response "Success" +// @Failure 400 {object} Response "Invalid client index" +// @Failure 404 {object} Response "Client not found" +// @Router /api/client/{index}/name [put] +func (ah *APIHandler) UpdateClientName(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + index, err := strconv.Atoi(vars["index"]) + if err != nil { + http.Error(w, "Invalid client index", http.StatusBadRequest) + return + } + + var req UpdateClientNameRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + allClients := ah.daemon.GetClientPool().GetAllClients() + if index < 0 || index >= len(allClients) { + http.Error(w, "Client not found", http.StatusNotFound) + return + } + + client := allClients[index] + client.SetNameOverride(req.NameOverride) + + w.WriteHeader(http.StatusOK) +} + // ExportSpammersRequest represents the request body for exporting spammers type ExportSpammersRequest struct { SpammerIDs []int64 `json:"spammer_ids,omitempty"` // If empty, exports all spammers diff --git a/webui/handlers/clients.go b/webui/handlers/clients.go index 945bb5a..9b99494 100644 --- a/webui/handlers/clients.go +++ b/webui/handlers/clients.go @@ -16,14 +16,15 @@ type ClientsPage struct { } type ClientsPageClient struct { - Index int `json:"index"` - Name string `json:"name"` - Group string `json:"group"` // First group for backward compatibility - Groups []string `json:"groups"` // All groups - Version string `json:"version"` - BlockHeight uint64 `json:"block_height"` - IsReady bool `json:"ready"` - Enabled bool `json:"enabled"` + Index int `json:"index"` + Name string `json:"name"` + Group string `json:"group"` // First group for backward compatibility + Groups []string `json:"groups"` // All groups + Version string `json:"version"` + BlockHeight uint64 `json:"block_height"` + IsReady bool `json:"ready"` + Enabled bool `json:"enabled"` + NameOverride string `json:"name_override,omitempty"` } // Clients will return the "clients" page using a go template @@ -64,13 +65,14 @@ func (fh *FrontendHandler) getClientsPageData(ctx context.Context) (*ClientsPage blockHeight, _ := client.GetLastBlockHeight() clientData := &ClientsPageClient{ - Index: idx, - Name: client.GetName(), - Group: client.GetClientGroup(), - Groups: client.GetClientGroups(), - BlockHeight: blockHeight, - IsReady: slices.Contains(goodClients, client), - Enabled: client.IsEnabled(), + Index: idx, + Name: client.GetName(), + Group: client.GetClientGroup(), + Groups: client.GetClientGroups(), + BlockHeight: blockHeight, + IsReady: slices.Contains(goodClients, client), + Enabled: client.IsEnabled(), + NameOverride: client.GetNameOverride(), } wg.Add(1) diff --git a/webui/handlers/docs/docs.go b/webui/handlers/docs/docs.go index 7292e0b..4ce9f15 100644 --- a/webui/handlers/docs/docs.go +++ b/webui/handlers/docs/docs.go @@ -117,6 +117,57 @@ const docTemplate = `{ } } }, + "/api/client/{index}/name": { + "put": { + "description": "Updates the name override for a specific client", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "summary": "Update client name override", + "operationId": "updateClientName", + "parameters": [ + { + "type": "integer", + "description": "Client index", + "name": "index", + "in": "path", + "required": true + }, + { + "description": "New name override", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.UpdateClientNameRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.Response" + } + }, + "400": { + "description": "Invalid client index", + "schema": { + "$ref": "#/definitions/api.Response" + } + }, + "404": { + "description": "Client not found", + "schema": { + "$ref": "#/definitions/api.Response" + } + } + } + } + }, "/api/clients": { "get": { "description": "Returns a list of all clients with their details", @@ -869,6 +920,9 @@ const docTemplate = `{ "name": { "type": "string" }, + "name_override": { + "type": "string" + }, "ready": { "type": "boolean" }, @@ -1032,6 +1086,21 @@ const docTemplate = `{ } } }, + "api.UpdateClientNameRequest": { + "type": "object", + "properties": { + "name_override": { + "type": "string" + }, + "groups": { + "description": "Multiple groups", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "api.UpdateSpammerRequest": { "type": "object", "properties": { diff --git a/webui/handlers/docs/swagger.json b/webui/handlers/docs/swagger.json index 1985392..8e36aa0 100644 --- a/webui/handlers/docs/swagger.json +++ b/webui/handlers/docs/swagger.json @@ -106,6 +106,57 @@ } } }, + "/api/client/{index}/name": { + "put": { + "description": "Updates the name override for a specific client", + "consumes": [ + "application/json" + ], + "tags": [ + "Client" + ], + "summary": "Update client name override", + "operationId": "updateClientName", + "parameters": [ + { + "type": "integer", + "description": "Client index", + "name": "index", + "in": "path", + "required": true + }, + { + "description": "New name override", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.UpdateClientNameRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.Response" + } + }, + "400": { + "description": "Invalid client index", + "schema": { + "$ref": "#/definitions/api.Response" + } + }, + "404": { + "description": "Client not found", + "schema": { + "$ref": "#/definitions/api.Response" + } + } + } + } + }, "/api/clients": { "get": { "description": "Returns a list of all clients with their details", @@ -858,6 +909,9 @@ "name": { "type": "string" }, + "name_override": { + "type": "string" + }, "ready": { "type": "boolean" }, @@ -1021,6 +1075,21 @@ } } }, + "api.UpdateClientNameRequest": { + "type": "object", + "properties": { + "name_override": { + "type": "string" + }, + "groups": { + "description": "Multiple groups", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "api.UpdateSpammerRequest": { "type": "object", "properties": { diff --git a/webui/handlers/docs/swagger.yaml b/webui/handlers/docs/swagger.yaml index 6513b4a..5f25d03 100644 --- a/webui/handlers/docs/swagger.yaml +++ b/webui/handlers/docs/swagger.yaml @@ -17,6 +17,8 @@ definitions: type: integer name: type: string + name_override: + type: string ready: type: boolean rpc_host: @@ -124,6 +126,16 @@ definitions: type: string type: array type: object + api.UpdateClientNameRequest: + properties: + name_override: + type: string + groups: + description: Multiple groups + items: + type: string + type: array + type: object api.UpdateSpammerRequest: properties: config: @@ -271,6 +283,40 @@ paths: summary: Update client group tags: - Client + /api/client/{index}/name: + put: + consumes: + - application/json + description: Updates the name override for a specific client + operationId: updateClientName + parameters: + - description: Client index + in: path + name: index + required: true + type: integer + - description: New name override + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.UpdateClientNameRequest' + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.Response' + "400": + description: Invalid client index + schema: + $ref: '#/definitions/api.Response' + "404": + description: Client not found + schema: + $ref: '#/definitions/api.Response' + summary: Update client name override + tags: + - Client /api/clients: get: description: Returns a list of all clients with their details diff --git a/webui/templates/clients/clients.html b/webui/templates/clients/clients.html index 758814c..af85ae4 100644 --- a/webui/templates/clients/clients.html +++ b/webui/templates/clients/clients.html @@ -21,7 +21,12 @@

Clients

{{ range $i, $client := .Clients }} {{ $client.Index }} - {{ $client.Name }} + + {{ $client.Name }} + {{ if $client.NameOverride }} + + {{ end }} +
{{ range $client.Groups }} @@ -50,6 +55,13 @@

Clients

+
+ + +