synapsecns/sanguine

View on GitHub
contrib/opbot/botmd/commands.go

Summary

Maintainability
D
2 days
Test Coverage
// Package botmd provides the bot server. Here botmd=cmd not markdown.
// nolint: forcetypeassert, mnd, cyclop
package botmd

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "regexp"
    "sort"
    "strings"
    "time"

    "github.com/dustin/go-humanize"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "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/core"
    "github.com/synapsecns/sanguine/core/retry"
    "github.com/synapsecns/sanguine/ethergo/chaindata"
    "github.com/synapsecns/sanguine/ethergo/submitter"
    rfqClient "github.com/synapsecns/sanguine/services/rfq/api/client"
    "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge"
)

func (b *Bot) requiresSignoz(definition *slacker.CommandDefinition) *slacker.CommandDefinition {
    if b.signozEnabled {
        return definition
    }
    return &slacker.CommandDefinition{
        Command:     definition.Command,
        Description: fmt.Sprintf("normally this would \"%s\", but signoz is not configured", definition.Description),
        Examples:    definition.Examples,
        Handler: func(ctx *slacker.CommandContext) {
            _, err := ctx.Response().Reply("cannot run command: signoz is not configured")
            if err != nil {
                log.Println(err)
            }
        },
    }
}

// nolint: traceCommand, gocognit
// TODO: add trace middleware.
func (b *Bot) traceCommand() *slacker.CommandDefinition {
    return b.requiresSignoz(&slacker.CommandDefinition{
        Command:     "trace {tags} {order}",
        Description: "find a transaction in signoz",
        Examples: []string{
            "trace transaction_id:0x1234@serviceName:rfq",
            "trace transaction_id:0x1234@serviceName:rfq a",
            "trace transaction_id:0x1234@serviceName:rfq asc",
        },
        Handler: func(ctx *slacker.CommandContext) {
            tags := stripLinks(ctx.Request().Param("tags"))
            splitTags := strings.Split(tags, "@")
            if len(splitTags) == 0 {
                _, err := ctx.Response().Reply("please provide tags in a key:value format")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            searchMap := make(map[string]string)
            for _, combinedTag := range splitTags {
                tag := strings.Split(combinedTag, ":")
                if len(tag) != 2 {
                    _, err := ctx.Response().Reply("please provide tags in a key:value format")
                    if err != nil {
                        log.Println(err)
                    }
                    return
                }
                searchMap[tag[0]] = tag[1]
            }

            // search for the transaction
            res, err := b.signozClient.SearchTraces(ctx.Context(), signoz.Last3Hr, searchMap)
            if err != nil {
                b.logger.Errorf(ctx.Context(), "error searching for the transaction: %v", err)
                _, err := ctx.Response().Reply("error searching for the transaction")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            if res.Status != "success" || res.Data.ContextTimeout || len(res.Data.Result) != 1 {
                _, err := ctx.Response().Reply(fmt.Sprintf("error searching for the transaction %s", res.Data.ContextTimeoutMessage))
                if err != nil {
                    log.Println(err)
                }
                return
            }

            traceList := res.Data.Result[0].List
            if len(traceList) == 0 {
                _, err := ctx.Response().Reply("no transaction found")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            order := strings.ToLower(ctx.Request().Param("order"))
            isAscending := order == "a" || order == "asc"
            if isAscending {
                sort.Slice(traceList, func(i, j int) bool {
                    return traceList[i].Timestamp.Before(traceList[j].Timestamp)
                })
            }

            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)
                spanID := results.Data["spanID"].(string)
                serviceName := results.Data["serviceName"].(string)

                url := fmt.Sprintf("%s/trace/%s?spanId=%s", b.cfg.SignozBaseURL, trace, spanID)
                traceName := fmt.Sprintf("<%s|%s>", url, results.Data["name"].(string))

                relativeTime := durafmt.Parse(time.Since(results.Timestamp)).LimitFirstN(1).String()

                slackBlocks = append(slackBlocks, slack.NewSectionBlock(nil, []*slack.TextBlockObject{
                    {
                        Type: slack.MarkdownType,
                        Text: fmt.Sprintf("*Name*: %s", traceName),
                    },
                    {
                        Type: slack.MarkdownType,
                        Text: fmt.Sprintf("*Service*: %s", serviceName),
                    },
                    {
                        Type: slack.MarkdownType,
                        Text: fmt.Sprintf("*When*: %s", fmt.Sprintf("%s ago", relativeTime)),
                    },
                }, nil))
            }

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

// nolint: gocognit
func (b *Bot) rfqLookupCommand() *slacker.CommandDefinition {
    return &slacker.CommandDefinition{
        Command:     "rfq <tx>",
        Description: "find a rfq transaction by either tx hash or txid from the rfq-indexer api",
        Examples: []string{
            "rfq 0x30f96b45ba689c809f7e936c140609eb31c99b182bef54fccf49778716a7e1ca",
        },
        Handler: func(ctx *slacker.CommandContext) {
            tx := stripLinks(ctx.Request().Param("tx"))

            res, status, err := b.rfqClient.GetRFQ(ctx.Context(), tx)
            if err != nil {
                b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err)
                _, err := ctx.Response().Reply(fmt.Sprintf("error fetching quote request %s", err.Error()))
                if err != nil {
                    log.Println(err)
                }
                return
            }

            var slackBlocks []slack.Block

            objects := []*slack.TextBlockObject{
                {
                    Type: slack.MarkdownType,
                    Text: fmt.Sprintf("*Relayer*: %s", res.BridgeRelay.Relayer),
                },
                {
                    Type: slack.MarkdownType,
                    Text: fmt.Sprintf("*Status*: %s", status),
                },
                {
                    Type: slack.MarkdownType,
                    Text: fmt.Sprintf("*TxID*: %s", toExplorerSlackLink(res.Bridge.TransactionID)),
                },
                {
                    Type: slack.MarkdownType,
                    //nolint: gosec
                    Text: fmt.Sprintf("*OriginTxHash*: %s", toTXSlackLink(res.BridgeRequest.TransactionHash, uint32(res.Bridge.OriginChainID))),
                },
                {
                    Type: slack.MarkdownType,
                    Text: fmt.Sprintf("*Estimated Tx Age*: %s", humanize.Time(time.Unix(res.BridgeRelay.BlockTimestamp, 0))),
                },
            }

            if status == "Requested" {
                objects = append(objects, &slack.TextBlockObject{
                    Type: slack.MarkdownType,
                    Text: "*DestTxHash*: not available",
                })
            } else {
                //nolint: gosec
                objects = append(objects, &slack.TextBlockObject{
                    Type: slack.MarkdownType,
                    Text: fmt.Sprintf("*DestTxHash*: %s", toTXSlackLink(res.BridgeRelay.TransactionHash, uint32(res.Bridge.DestChainID))),
                })
            }

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

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

// nolint: gocognit, cyclop, gosec.
func (b *Bot) rfqRefund() *slacker.CommandDefinition {
    return &slacker.CommandDefinition{
        Command:     "refund <tx>",
        Description: "refund a quote request",
        Examples:    []string{"refund 0x1234"},
        Handler: func(ctx *slacker.CommandContext) {
            tx := stripLinks(ctx.Request().Param("tx"))

            if len(tx) == 0 {
                _, err := ctx.Response().Reply("please provide a tx hash")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            rawRequest, _, err := b.rfqClient.GetRFQ(ctx.Context(), tx)
            if err != nil {
                b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err)
                _, err := ctx.Response().Reply("error fetching quote request")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            //nolint: gosec
            fastBridgeContractOrigin, err := b.makeFastBridge(ctx.Context(), uint32(rawRequest.Bridge.OriginChainID))
            if err != nil {
                _, err := ctx.Response().Reply(err.Error())
                if err != nil {
                    log.Println(err)
                }
                return
            }

            isScreened, err := b.screener.ScreenAddress(ctx.Context(), rawRequest.Bridge.Sender)
            if err != nil {
                _, err := ctx.Response().Reply("error screening address")
                if err != nil {
                    log.Println(err)
                }
                return
            }
            if isScreened {
                _, err := ctx.Response().Reply("address cannot be refunded")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            //nolint:gosec
            fastBridgeContractDest, err := b.makeFastBridge(ctx.Context(), uint32(rawRequest.Bridge.DestChainID))
            if err != nil {
                _, err := ctx.Response().Reply(err.Error())
                if err != nil {
                    log.Println(err)
                }
                return
            }
            txBz, err := core.BytesToArray(common.Hex2Bytes(rawRequest.Bridge.TransactionID[2:]))
            if err != nil {
                _, err := ctx.Response().Reply("error converting tx id")
                if err != nil {
                    log.Println(err)
                }
                return
            }
            isRelayed, err := fastBridgeContractDest.BridgeRelays(nil, txBz)
            if err != nil {
                _, err := ctx.Response().Reply("error fetching bridge relays")
                if err != nil {
                    log.Println(err)
                }
                return
            }
            if isRelayed {
                _, err := ctx.Response().Reply("transaction has already been relayed")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            nonce, err := b.submitter.SubmitTransaction(
                ctx.Context(),
                big.NewInt(int64(rawRequest.Bridge.OriginChainID)),
                func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) {
                    tx, err = fastBridgeContractOrigin.Refund(transactor, common.Hex2Bytes(rawRequest.Bridge.Request[2:]))
                    if err != nil {
                        return nil, fmt.Errorf("error submitting refund: %w", err)
                    }
                    return tx, nil
                })
            if err != nil {
                log.Printf("error submitting refund: %v\n", err)
                _, err := ctx.Response().Reply("error submitting refund")
                if err != nil {
                    log.Println(err)
                }
                return
            }

            var status submitter.SubmissionStatus
            err = retry.WithBackoff(
                ctx.Context(),
                func(ctx context.Context) error {
                    status, err = b.submitter.GetSubmissionStatus(ctx, big.NewInt(int64(rawRequest.Bridge.OriginChainID)), nonce)
                    if err != nil || !status.HasTx() {
                        b.logger.Errorf(ctx, "error fetching quote request: %v", err)
                        return fmt.Errorf("error fetching quote request: %w", err)
                    } else if !status.HasTx() {
                        return fmt.Errorf("no transaction hash found yet")
                    }
                    return nil
                },
                retry.WithMaxTotalTime(1*time.Minute),
            )

            if err != nil {
                b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err)
                _, err := ctx.Response().Reply(fmt.Sprintf("refund submitted with nonce %d", nonce))
                if err != nil {
                    b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err)
                }
                return
            }

            //nolint: gosec
            _, err = ctx.Response().Reply(fmt.Sprintf("refund submitted: %s", toTXSlackLink(status.TxHash().String(), uint32(rawRequest.Bridge.OriginChainID))))
            if err != nil {
                log.Println(err)
            }

        },
    }
}

func (b *Bot) makeFastBridge(ctx context.Context, chainID uint32) (*fastbridge.FastBridge, error) {
    client, err := rfqClient.NewUnauthenticatedClient(b.handler, b.cfg.RFQApiURL)
    if err != nil {
        return nil, fmt.Errorf("error creating rfq client: %w", err)
    }

    contracts, err := client.GetRFQContracts(ctx)
    if err != nil {
        return nil, fmt.Errorf("error fetching rfq contracts: %w", err)
    }

    chainClient, err := b.rpcClient.GetChainClient(ctx, int(chainID))
    if err != nil {
        return nil, fmt.Errorf("error getting chain client for chain ID %d: %w", chainID, err)
    }

    contractAddress, ok := contracts.Contracts[chainID]
    if !ok {
        return nil, fmt.Errorf("no contract address for chain ID")
    }

    fastBridgeHandle, err := fastbridge.NewFastBridge(common.HexToAddress(contractAddress), chainClient)
    if err != nil {
        return nil, fmt.Errorf("error creating fast bridge for chain ID %d: %w", chainID, err)
    }

    return fastBridgeHandle, nil

}

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

    return fmt.Sprintf("<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("<%s|%s>", url, txHash)
}

func stripLinks(input string) string {
    linkRegex := regexp.MustCompile(`<https?://[^|>]+\|([^>]+)>`)
    return linkRegex.ReplaceAllString(input, "$1")
}