Skip to content

Commit

Permalink
add minimal rfq relayer query interface (#2772)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Trajan0x <trajan0x@users.noreply.github.com>
  • Loading branch information
trajan0x and trajan0x authored Jun 22, 2024
1 parent afa9e47 commit 99185a5
Show file tree
Hide file tree
Showing 15 changed files with 1,567 additions and 33 deletions.
75 changes: 74 additions & 1 deletion contrib/opbot/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,74 @@
# Op Bot is a slack bot
# OpBot

OpBot is a Slack bot written in Go that interacts with the Signoz trace API to provide various functionalities, including searching for transactions based on user-provided tags. This bot is designed to help teams monitor and manage their operations more effectively by integrating with Slack and Signoz.

## Features

- **Slack Integration**: Interact with the bot directly from Slack.
- **Signoz Integration**: Search for transactions and traces using the Signoz API.
- **Configuration Management**: Easily manage configuration through YAML files.
- **Metrics Handling**: Integrated with metrics handling for better monitoring.

## Installation

1. **Clone the repository**:
```sh
git clone https://github.com/synapsecns/sanguine.git
cd sanguine/contrib/opbot
```

2. **Install dependencies**:
Ensure you have Go installed (version 1.22.4 or later). Then, run:
```sh
go mod tidy
```

3. **Build the bot**:
```sh
go build -o opbot main.go
```

## Configuration

OpBot uses a YAML configuration file to manage its settings. The configuration file should be named `config.yml` and placed in the same directory as the executable.

### Example `config.yml`

```yaml
slack_bot_token: "your-slack-bot-token"
slack_app_token: "your-slack-app-token"
signoz_email: "your-signoz-email"
signoz_password: "your-signoz-password"
signoz_base_url: "https://signoz.example.com"
```

### Configuration Fields

- `slack_bot_token`: The token for your Slack bot.
- `slack_app_token`: The token for your Slack app.
- `signoz_email`: The email address used to log in to Signoz.
- `signoz_password`: The password used to log in to Signoz.
- `signoz_base_url`: The base URL for the Signoz API.

## Usage

1. **Start the bot**:
```sh
./opbot start --config config.yml
```

2. **Interact with the bot in Slack**:
- Use commands to search for transactions in Signoz.
- Example command: `/opbot search --tag key:value`

## Development

### Directory Structure

- **`cmd`**: Contains the command line interface for the bot.
- **`config`**: Provides functionality to read and write configuration files.
- **`botmd`**: Contains the main bot server implementation.
- **`metadata`**: Provides metadata services for the bot.
- **`signoz`**: Contains the Signoz client for interacting with the Signoz API.

Feel free to reach out if you have any questions or need further assistance!
8 changes: 7 additions & 1 deletion contrib/opbot/botmd/botmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ func NewBot(handler metrics.Handler, cfg config.Config) Bot {

bot.signozClient = signoz.NewClientFromUser(handler, cfg.SignozBaseURL, cfg.SignozEmail, cfg.SignozPassword)

server.AddCommand(bot.traceCommand())
bot.addCommands(bot.traceCommand(), bot.rfqLookupCommand())

return bot
}

func (b *Bot) addCommands(commands ...*slacker.CommandDefinition) {
for _, command := range commands {
b.server.AddCommand(command)
}
}

// Start starts the bot server.
// nolint: wrapcheck
func (b *Bot) Start(ctx context.Context) error {
Expand Down
135 changes: 134 additions & 1 deletion contrib/opbot/botmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ package botmd

import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/hako/durafmt"
"github.com/slack-go/slack"
"github.com/slack-io/slacker"
"github.com/synapsecns/sanguine/contrib/opbot/signoz"
"github.com/synapsecns/sanguine/ethergo/chaindata"
"github.com/synapsecns/sanguine/services/rfq/relayer/relapi"
"log"
"strings"
"sync"
"time"
)

Expand Down Expand Up @@ -73,7 +77,7 @@ func (b *Bot) traceCommand() *slacker.CommandDefinition {
return
}

slackBlocks := []slack.Block{slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, "Traces", false, false))}
slackBlocks := []slack.Block{slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprintf("Traces for %s", tags), false, false))}

for _, results := range traceList {
trace := results.Data["traceID"].(string)
Expand Down Expand Up @@ -109,3 +113,132 @@ func (b *Bot) traceCommand() *slacker.CommandDefinition {
},
}
}

func (b *Bot) rfqLookupCommand() *slacker.CommandDefinition {
return &slacker.CommandDefinition{
Command: "rfq <tx>",
Description: "find a rfq transaction by either tx hash or txid on all configured relayers",
Examples: []string{
"rfq 0x30f96b45ba689c809f7e936c140609eb31c99b182bef54fccf49778716a7e1ca",
},
Handler: func(ctx *slacker.CommandContext) {
type Status struct {
relayer string
*relapi.GetQuoteRequestStatusResponse
}

var statuses []Status
var sliceMux sync.Mutex

if len(b.cfg.RelayerURLS) == 0 {
_, err := ctx.Response().Reply("no relayer urls configured")
if err != nil {
log.Println(err)
}
return
}

tx := ctx.Request().Param("tx")

var wg sync.WaitGroup
// 2 routines per relayer, one for tx hashh one for tx id
wg.Add(len(b.cfg.RelayerURLS) * 2)
for _, relayer := range b.cfg.RelayerURLS {
client := relapi.NewRelayerClient(b.handler, relayer)
go func() {
defer wg.Done()
res, err := client.GetQuoteRequestStatusByTxHash(ctx.Context(), tx)
if err != nil {
log.Printf("error fetching quote request status by tx hash: %v\n", err)
return
}
sliceMux.Lock()
defer sliceMux.Unlock()
statuses = append(statuses, Status{relayer: relayer, GetQuoteRequestStatusResponse: res})
}()

go func() {
defer wg.Done()
res, err := client.GetQuoteRequestStatusByTxID(ctx.Context(), tx)
if err != nil {
log.Printf("error fetching quote request status by tx id: %v\n", err)
return
}
sliceMux.Lock()
defer sliceMux.Unlock()
statuses = append(statuses, Status{relayer: relayer, GetQuoteRequestStatusResponse: res})
}()
}
wg.Wait()

if len(statuses) == 0 {
_, err := ctx.Response().Reply("no quote request found")
if err != nil {
log.Println(err)
}
return
}

var slackBlocks []slack.Block
for _, status := range statuses {
objects := []*slack.TextBlockObject{
{
Type: slack.MarkdownType,
Text: fmt.Sprintf("*Relayer*: %s", status.relayer),
},
{
Type: slack.MarkdownType,
Text: fmt.Sprintf("*Status*: %s", status.Status),
},
{
Type: slack.MarkdownType,
Text: fmt.Sprintf("*TxID*: %s", toExplorerSlackLink(status.TxID)),
},
{
Type: slack.MarkdownType,
Text: fmt.Sprintf("*OriginTxHash*: %s", toTXSlackLink(status.OriginTxHash, status.OriginChainID)),
},
}

if status.DestTxHash == (common.Hash{}).String() {
objects = append(objects, &slack.TextBlockObject{
Type: slack.MarkdownType,
Text: "*DestTxHash*: not available",
})
} else {
objects = append(objects, &slack.TextBlockObject{
Type: slack.MarkdownType,
Text: fmt.Sprintf("*DestTxHash*: %s", toTXSlackLink(status.DestTxHash, status.DestChainID)),
})
}

slackBlocks = append(slackBlocks, slack.NewSectionBlock(nil, objects, nil))
}

_, err := ctx.Response().ReplyBlocks(slackBlocks)
if err != nil {
log.Println(err)
}
}}
}

func toExplorerSlackLink(ogHash string) string {
rfqHash := strings.ToUpper(ogHash)
// cut off 0x
if len(rfqHash) > 0 {
rfqHash = strings.ToLower(rfqHash[2:])
}

return fmt.Sprintf("<https://anon.to/?https://explorer.synapseprotocol.com/tx/%s|%s>", rfqHash, ogHash)
}

// produce a salck link if the explorer exists.
func toTXSlackLink(txHash string, chainID uint32) string {
url := chaindata.ToTXLink(int64(chainID), txHash)
if url == "" {
return txHash
}

// TODO: remove when we can contorl unfurl
return fmt.Sprintf("<https://anon.to/?%s|%s>", url, txHash)
}
3 changes: 2 additions & 1 deletion contrib/opbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ type Config struct {
// inject only with init container!
SignozPassword string `yaml:"signoz_password"`
// SignozBaseURL is the base url of the signoz instance.
SignozBaseURL string `yaml:"signoz_base_url"`
SignozBaseURL string `yaml:"signoz_base_url"`
RelayerURLS []string `yaml:"rfq_relayer_urls"`
}
Loading

0 comments on commit 99185a5

Please sign in to comment.