Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #64 from primevprotocol/iowar/searcher-cli_opt2
Browse files Browse the repository at this point in the history
feat: searcher cli opt2
  • Loading branch information
iowar authored Oct 23, 2023
2 parents 5041a00 + 3e72ca7 commit 4ec4260
Show file tree
Hide file tree
Showing 2 changed files with 317 additions and 0 deletions.
45 changes: 45 additions & 0 deletions examples/searcher_cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Searcher CLI
Searcher CLI is a command-line tool used to interact with a gRPC searcher server.

## Usage
Searcher CLI can be used with the following commands:

### 1. Send Bid
Used to send a bid.

```bash
searcher-cli send-bid --txhash <transaction_hash> --amount <bid_amount> --block <block_number>
```
* `--txhash`: Transaction hash.
* `--amount`: Bid amount.
* `--block`: Block number.

### 2. Check Status
Used to check the status of the gRPC searcher server.

```bash
searcher-cli status
```

### 3. Send Random Bid
Used to send a random bid.

```bash
searcher-cli send-rand-bid
```

## Configuration
Configuration options can be set using the config.yml file. An example configuration file is as follows:

```yaml
server_address: "localhost:13524"
log_fmt: "text"
log_level: "info"
```
* `server_address`: The address and port of the gRPC searcher server.
* `log_fmt`: Log format (text or json).
* `log_level`: Log level (debug, info, warn, error).

You can modify these settings to suit your specific environment and preferences.

272 changes: 272 additions & 0 deletions examples/searcher_cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package main

import (
"context"
"fmt"
"io"
"log/slog"
"math/rand"
"os"
"time"

pb "github.com/primevprotocol/mev-commit/gen/go/rpc/searcherapi/v1"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v2"
)

var (
txHash string
amount int64
blockNumber int64
)

type config struct {
ServerAddress string `json:"server_address" yaml:"server_address"`
LogFmt string `json:"log_fmt" yaml:"log_fmt"`
LogLevel string `json:"log_level" yaml:"log_level"`
}

var (
optionConfig = &cli.StringFlag{
Name: "config",
Usage: "path to config file",
Required: true,
EnvVars: []string{"SEARCHER_CLI_CONFIG"},
}
)

func main() {
app := cli.NewApp()
app.Name = "searcher-cli"
app.Usage = "A CLI tool for interacting with a gRPC searcher server"
app.Version = "1.0.0"

var (
cfg config
logger *slog.Logger
)

app.Flags = []cli.Flag{
optionConfig,
}

app.Before = func(c *cli.Context) error {
configFile := c.String(optionConfig.Name)
fmt.Printf("using configuration file: %s\n", configFile)

buf, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("failed to read config file at '%s': %w", configFile, err)
}

if err := yaml.Unmarshal(buf, &cfg); err != nil {
return fmt.Errorf("failed to unmarshal config file at '%s': %w", configFile, err)
}

if err := checkConfig(&cfg); err != nil {
return fmt.Errorf("failed to unmarshal config file at '%s': %w", configFile, err)
}

logger, err = newLogger(cfg.LogLevel, cfg.LogFmt, c.App.Writer)
if err != nil {
return fmt.Errorf("failed to create logger: %w", err)
}

return nil
}

app.Commands = []*cli.Command{
{
Name: "send-bid",
Usage: "Send a bid to the gRPC searcher server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "txhash",
Usage: "Transaction hash",
Destination: &txHash,
},
&cli.Int64Flag{
Name: "amount",
Usage: "Bid amount",
Destination: &amount,
},
&cli.Int64Flag{
Name: "block",
Usage: "Block number",
Destination: &blockNumber,
},
},
Action: func(c *cli.Context) error {
if txHash == "" || amount == 0 || blockNumber == 0 {
return fmt.Errorf("Missing required arguments. Please provide --txhash, --amount, and --block.")
}

creds := insecure.NewCredentials()
conn, err := grpc.Dial(cfg.ServerAddress, grpc.WithTransportCredentials(creds))
if err != nil {
return err
}
defer conn.Close()

client := pb.NewSearcherClient(conn)

bid := &pb.Bid{
TxHash: txHash,
Amount: amount,
BlockNumber: blockNumber,
}

ctx := context.Background()
stream, err := client.SendBid(ctx, bid)
if err != nil {
return err
}

preConfirmation, err := stream.Recv()
if err != nil {
return err
}

logger.Info("received preconfirmation", "preconfirmation", preConfirmation)
return nil
},
},
{
// NOTE: (@iowar) By sending an empty Bid request, the status of the RPC
// server is being checked. Instead, a ping request can be defined within
// the searcher proto or a better solution can be found. Seeking the team's
// opinion on this
Name: "status",
Usage: "Check the status of the gRPC searcher server",
Action: func(c *cli.Context) error {
creds := insecure.NewCredentials()
conn, err := grpc.Dial(cfg.ServerAddress, grpc.WithTransportCredentials(creds))
if err != nil {
return err
}
defer conn.Close()

client := pb.NewSearcherClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
defer cancel()

_, err = client.SendBid(ctx, &pb.Bid{})
if err != nil {
logger.Info("gRPC searcher server is not reachable", "server", cfg.ServerAddress)
return nil
}

logger.Info("gRPC searcher server is up and running", "server", cfg.ServerAddress)
return nil
},
},
{
Name: "send-rand-bid",
Usage: "Send a random bid to the gRPC searcher server",
Action: func(c *cli.Context) error {
randSource := rand.NewSource(time.Now().UnixNano())
randGenerator := rand.New(randSource)

txHash = generateTxHash(randGenerator)
amount = randGenerator.Int63n(1000) + 1
blockNumber = randGenerator.Int63n(100000) + 1

creds := insecure.NewCredentials()
conn, err := grpc.Dial(cfg.ServerAddress, grpc.WithTransportCredentials(creds))
if err != nil {
return err
}
defer conn.Close()

client := pb.NewSearcherClient(conn)

bid := &pb.Bid{
TxHash: txHash,
Amount: amount,
BlockNumber: blockNumber,
}

ctx := context.Background()
stream, err := client.SendBid(ctx, bid)
if err != nil {
return err
}

preConfirmation, err := stream.Recv()
if err != nil {
return err
}

logger.Info("received preconfirmation", "preconfirmation", preConfirmation)
return nil
},
},
}

err := app.Run(os.Args)
if err != nil {
fmt.Fprintf(app.Writer, "exited with error: %v\n", err)
}
}

func checkConfig(cfg *config) error {
if cfg.ServerAddress == "" {
return fmt.Errorf("server_address is required")
}

if cfg.LogFmt == "" {
cfg.LogFmt = "text"
}

if cfg.LogLevel == "" {
cfg.LogLevel = "info"
}

return nil
}

func newLogger(lvl, logFmt string, sink io.Writer) (*slog.Logger, error) {
var (
level = new(slog.LevelVar)
handler slog.Handler
)

switch lvl {
case "debug":
level.Set(slog.LevelDebug)
case "info":
level.Set(slog.LevelInfo)
case "warn":
level.Set(slog.LevelWarn)
case "error":
level.Set(slog.LevelError)
default:
return nil, fmt.Errorf("invalid log level: %s", lvl)
}

switch logFmt {
case "text":
handler = slog.NewTextHandler(sink, &slog.HandlerOptions{Level: level})
case "none":
fallthrough
case "json":
handler = slog.NewJSONHandler(sink, &slog.HandlerOptions{Level: level})
default:
return nil, fmt.Errorf("invalid log format: %s", logFmt)
}

return slog.New(handler), nil
}

func generateTxHash(r *rand.Rand) string {
const charset = "0123456789abcdef"
result := make([]byte, 66)
for i := range result {
result[i] = charset[r.Intn(len(charset))]
}
result = append([]byte("0x"), result...)
return string(result)
}

0 comments on commit 4ec4260

Please sign in to comment.