diff --git a/contrib/opbot/botmd/botmd.go b/contrib/opbot/botmd/botmd.go index 24b9236e77..0b6d6e4c03 100644 --- a/contrib/opbot/botmd/botmd.go +++ b/contrib/opbot/botmd/botmd.go @@ -2,10 +2,19 @@ package botmd import ( "context" + "fmt" "github.com/slack-io/slacker" "github.com/synapsecns/sanguine/contrib/opbot/config" "github.com/synapsecns/sanguine/contrib/opbot/signoz" + "github.com/synapsecns/sanguine/core/dbcommon" "github.com/synapsecns/sanguine/core/metrics" + signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" + "github.com/synapsecns/sanguine/ethergo/signer/signer" + "github.com/synapsecns/sanguine/ethergo/submitter" + submitterdb "github.com/synapsecns/sanguine/ethergo/submitter/db" + cctpSql "github.com/synapsecns/sanguine/services/cctp-relayer/db/sql" + omnirpcClient "github.com/synapsecns/sanguine/services/omnirpc/client" + "golang.org/x/sync/errgroup" ) // Bot represents the bot server. @@ -15,6 +24,10 @@ type Bot struct { cfg config.Config signozClient *signoz.Client signozEnabled bool + rpcClient omnirpcClient.RPCClient + signer signer.Signer + submitter submitter.TransactionSubmitter + db submitterdb.SubmitterDBFactory } // NewBot creates a new bot server. @@ -32,8 +45,10 @@ func NewBot(handler metrics.Handler, cfg config.Config) Bot { bot.signozEnabled = true } + bot.rpcClient = omnirpcClient.NewOmnirpcClient(cfg.OmniRPCURL, handler, omnirpcClient.WithCaptureReqRes()) + bot.addMiddleware(bot.tracingMiddleware(), bot.metricsMiddleware()) - bot.addCommands(bot.traceCommand(), bot.rfqLookupCommand()) + bot.addCommands(bot.traceCommand(), bot.rfqLookupCommand(), bot.rfqRefund()) return bot } @@ -53,6 +68,33 @@ func (b *Bot) addCommands(commands ...*slacker.CommandDefinition) { // Start starts the bot server. // nolint: wrapcheck func (b *Bot) Start(ctx context.Context) error { + var err error + b.signer, err = signerConfig.SignerFromConfig(ctx, b.cfg.Signer) + if err != nil { + return fmt.Errorf("failed to create signer: %w", err) + } + + dbType, err := dbcommon.DBTypeFromString(b.cfg.Database.Type) + if err != nil { + return fmt.Errorf("could not get db type: %w", err) + } + + store, err := cctpSql.Connect(ctx, dbType, b.cfg.Database.DSN, b.handler) + if err != nil { + return fmt.Errorf("could not connect to database: %w", err) + } + + b.submitter = submitter.NewTransactionSubmitter(b.handler, b.signer, b.rpcClient, store.SubmitterDB(), &b.cfg.SubmitterConfig) + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return b.submitter.Start(ctx) + }) + + g.Go(func() error { + return b.server.Listen(ctx) + }) + // nolint: wrapcheck - return b.server.Listen(ctx) + return g.Wait() } diff --git a/contrib/opbot/botmd/commands.go b/contrib/opbot/botmd/commands.go index d2e19039a6..e9a489303b 100644 --- a/contrib/opbot/botmd/commands.go +++ b/contrib/opbot/botmd/commands.go @@ -4,15 +4,21 @@ package botmd import ( "fmt" + "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/ethergo/chaindata" + rfqClient "github.com/synapsecns/sanguine/services/rfq/api/client" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" "log" + "math/big" "regexp" + "strconv" "strings" "sync" "time" @@ -239,6 +245,80 @@ func (b *Bot) rfqLookupCommand() *slacker.CommandDefinition { }} } +func (b *Bot) rfqRefund() *slacker.CommandDefinition { + return &slacker.CommandDefinition{ + Command: "refund ", + Description: "refund a quote request", + Examples: []string{"refund 0x1234"}, + Handler: func(ctx *slacker.CommandContext) { + client, err := rfqClient.NewUnauthenticatedClient(b.handler, b.cfg.RFQApiURL) + if err != nil { + log.Printf("error creating rfq client: %v\n", err) + return + } + + contracts, err := client.GetRFQContracts(ctx.Context()) + if err != nil { + log.Printf("error fetching rfq contracts: %v\n", err) + return + } + + tx := stripLinks(ctx.Request().Param("tx")) + originChainIDStr := ctx.Request().Param("origin_chainid") + + originChainID, err := strconv.Atoi(originChainIDStr) + if err != nil { + _, err := ctx.Response().Reply("origin_chainid must be a number") + if err != nil { + log.Println(err) + } + return + } + + chainClient, err := b.rpcClient.GetChainClient(ctx.Context(), originChainID) + if err != nil { + log.Printf("error getting chain client: %v\n", err) + return + } + + contractAddress, ok := contracts.Contracts[uint32(originChainID)] + if !ok { + _, err := ctx.Response().Reply("contract address not found") + if err != nil { + log.Println(err) + } + return + } + + fastBridgeHandle, err := fastbridge.NewFastBridge(common.HexToAddress(contractAddress), chainClient) + if err != nil { + log.Printf("error creating fast bridge: %v\n", err) + return + } + + for _, relayer := range b.cfg.RelayerURLS { + relClient := relapi.NewRelayerClient(b.handler, relayer) + qr, err := relClient.GetQuoteRequestByTXID(ctx.Context(), tx) + if err != nil { + log.Printf("error fetching quote request: %v\n", err) + continue + } + + nonce, err := b.submitter.SubmitTransaction(ctx.Context(), big.NewInt(int64(originChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + return fastBridgeHandle.Refund(transactor, common.Hex2Bytes(qr.QuoteRequestRaw)) + }) + if err != nil { + log.Printf("error submitting refund: %v\n", err) + continue + } + + _, err = ctx.Response().Reply(fmt.Sprintf("refund submitted with nonce %d", nonce)) + return + } + }, + } +} + func toExplorerSlackLink(ogHash string) string { rfqHash := strings.ToUpper(ogHash) // cut off 0x diff --git a/contrib/opbot/config/config.go b/contrib/opbot/config/config.go index 5701c876ef..d071dc3fba 100644 --- a/contrib/opbot/config/config.go +++ b/contrib/opbot/config/config.go @@ -1,6 +1,11 @@ // Package config provides a simple way to read and write configuration files. package config +import ( + "github.com/synapsecns/sanguine/ethergo/signer/config" + submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" +) + // Config represents the configuration of the application. type Config struct { // SlackBotToken is the token of the slack bot. @@ -18,4 +23,20 @@ type Config struct { SignozBaseURL string `yaml:"signoz_base_url"` // RelayerURLS is the list of RFQ relayer URLs. RelayerURLS []string `yaml:"rfq_relayer_urls"` + // RFQApiURL is the URL of the RFQ API. + RFQApiURL string `yaml:"rfq_api_url"` + // OmniRPCURL is the URL of the Omni RPC. + OmniRPCURL string `yaml:"omni_rpc_url"` + // Signer is the signer config. + Signer config.SignerConfig `yaml:"signer"` + // SubmitterConfig is the submitter config. + SubmitterConfig submitterConfig.Config `yaml:"submitter_config"` + // Database is the database config. + Database DatabaseConfig `yaml:"database"` +} + +// DatabaseConfig represents the configuration for the database. +type DatabaseConfig struct { + Type string `yaml:"type"` + DSN string `yaml:"dsn"` // Data Source Name } diff --git a/contrib/opbot/go.mod b/contrib/opbot/go.mod index a118d88166..c240fe4fee 100644 --- a/contrib/opbot/go.mod +++ b/contrib/opbot/go.mod @@ -271,7 +271,6 @@ replace ( // later versions give errors on uint64 being too high. github.com/brianvoe/gofakeit/v6 => github.com/brianvoe/gofakeit/v6 v6.9.0 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 - github.com/slack-go/slack => github.com/slack-go/slack v0.12.2 github.com/slack-io/slacker => github.com/slack-io/slacker v0.1.1-0.20240701203341-bd3ee211e9d2 github.com/synapsecns/sanguine/core => ./../../core github.com/synapsecns/sanguine/ethergo => ./../../ethergo diff --git a/contrib/opbot/go.sum b/contrib/opbot/go.sum index c89db77f7f..8722dbd416 100644 --- a/contrib/opbot/go.sum +++ b/contrib/opbot/go.sum @@ -990,8 +990,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= -github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ= +github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/slack-io/commander v0.0.0-20231120025847-9fd78b4b2d54 h1:aRc+G2mUb697z6bR09Roq6kP08suJulgNo00SGhAsfM= github.com/slack-io/commander v0.0.0-20231120025847-9fd78b4b2d54/go.mod h1:aHmXZnL/ELKlfMybblXnnCl+IeHQ+gMb++DT7ujIGlA= github.com/slack-io/proper v0.0.0-20231119200853-f78ba4fc878f h1:wiEJBKJKvMOeE9KtLV7iwtHOsNXDBZloL+FPlkZ6vnA= diff --git a/contrib/opbot/sql/base/base.go b/contrib/opbot/sql/base/base.go new file mode 100644 index 0000000000..e73ca70a4e --- /dev/null +++ b/contrib/opbot/sql/base/base.go @@ -0,0 +1,38 @@ +package base + +import ( + "github.com/synapsecns/sanguine/core/metrics" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" + "github.com/synapsecns/sanguine/ethergo/submitter/db/txdb" + "gorm.io/gorm" +) + +// Store is a store that implements an underlying gorm db. +type Store struct { + db *gorm.DB + metrics metrics.Handler + submitterStore submitterDB.Service +} + +// NewStore creates a new store. +func NewStore(db *gorm.DB, metrics metrics.Handler) *Store { + txDB := txdb.NewTXStore(db, metrics) + return &Store{db: db, metrics: metrics, submitterStore: txDB} +} + +// DB gets the database object for mutation outside of the lib. +func (s Store) DB() *gorm.DB { + return s.db +} + +// SubmitterDB gets the submitter database object for mutation outside of the lib. +func (s Store) SubmitterDB() submitterDB.Service { + return s.submitterStore +} + +// GetAllModels gets all models to migrate. +// see: https://medium.com/@SaifAbid/slice-interfaces-8c78f8b6345d for an explanation of why we can't do this at initialization time +func GetAllModels() (allModels []interface{}) { + allModels = append(allModels, txdb.GetAllModels()...) + return allModels +} diff --git a/contrib/opbot/sql/base/doc.go b/contrib/opbot/sql/base/doc.go new file mode 100644 index 0000000000..9b758883a1 --- /dev/null +++ b/contrib/opbot/sql/base/doc.go @@ -0,0 +1,2 @@ +// Package base contains the base sql implementation +package base diff --git a/contrib/opbot/sql/doc.go b/contrib/opbot/sql/doc.go new file mode 100644 index 0000000000..9c3daf2957 --- /dev/null +++ b/contrib/opbot/sql/doc.go @@ -0,0 +1,2 @@ +// Package sql provides a common interface for starting sql-lite databases +package sql diff --git a/contrib/opbot/sql/mysql/doc.go b/contrib/opbot/sql/mysql/doc.go new file mode 100644 index 0000000000..a6b8106850 --- /dev/null +++ b/contrib/opbot/sql/mysql/doc.go @@ -0,0 +1,2 @@ +// Package mysql contains a mysql db +package mysql diff --git a/contrib/opbot/sql/mysql/store.go b/contrib/opbot/sql/mysql/store.go new file mode 100644 index 0000000000..5ee8625a81 --- /dev/null +++ b/contrib/opbot/sql/mysql/store.go @@ -0,0 +1,66 @@ +package mysql + +import ( + "context" + "fmt" + "github.com/synapsecns/sanguine/contrib/opbot/sql/base" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" + "time" + + "github.com/ipfs/go-log" + common_base "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +// Logger is the mysql logger. +var logger = log.Logger("synapse-mysql") + +// NewMysqlStore creates a new mysql store for a given data store. +func NewMysqlStore(ctx context.Context, dbURL string, handler metrics.Handler) (*Store, error) { + logger.Debug("create mysql store") + + gdb, err := gorm.Open(mysql.Open(dbURL), &gorm.Config{ + Logger: common_base.GetGormLogger(logger), + FullSaveAssociations: true, + NamingStrategy: NamingStrategy, + NowFunc: time.Now, + }) + + if err != nil { + return nil, fmt.Errorf("could not create mysql connection: %w", err) + } + + sqlDB, err := gdb.DB() + if err != nil { + return nil, fmt.Errorf("could not get sql db: %w", err) + } + + // fixes a timeout issue https://stackoverflow.com/a/42146536 + sqlDB.SetMaxIdleConns(MaxIdleConns) + sqlDB.SetConnMaxLifetime(time.Hour) + + handler.AddGormCallbacks(gdb) + + err = gdb.WithContext(ctx).AutoMigrate(base.GetAllModels()...) + if err != nil { + return nil, fmt.Errorf("could not migrate on mysql: %w", err) + } + + return &Store{base.NewStore(gdb, handler)}, nil +} + +// Store is the mysql store. It extends the bsae store for mysql queries. +type Store struct { + *base.Store +} + +// MaxIdleConns is exported here for testing. Tests execute too slowly with a reconnect each time. +var MaxIdleConns = 10 + +// NamingStrategy is for table prefixes. +var NamingStrategy = schema.NamingStrategy{} + +var _ submitterDB.SubmitterDBFactory = &Store{} diff --git a/contrib/opbot/sql/sqlite/doc.go b/contrib/opbot/sql/sqlite/doc.go new file mode 100644 index 0000000000..d30fb340b9 --- /dev/null +++ b/contrib/opbot/sql/sqlite/doc.go @@ -0,0 +1,2 @@ +// Package sqlite implements the sqlite package +package sqlite diff --git a/contrib/opbot/sql/sqlite/sqlite.go b/contrib/opbot/sql/sqlite/sqlite.go new file mode 100644 index 0000000000..e3ebada5ef --- /dev/null +++ b/contrib/opbot/sql/sqlite/sqlite.go @@ -0,0 +1,62 @@ +package sqlite + +import ( + "context" + "fmt" + "github.com/synapsecns/sanguine/contrib/opbot/sql/base" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" + "os" + + "github.com/ipfs/go-log" + common_base "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// Store is the sqlite store. It extends the base store for sqlite specific queries. +type Store struct { + *base.Store +} + +var logger = log.Logger("scribe-sqlite") + +// NewSqliteStore creates a new sqlite data store. +func NewSqliteStore(parentCtx context.Context, dbPath string, handler metrics.Handler, skipMigrations bool) (_ *Store, err error) { + logger.Debugf("creating sqlite store at %s", dbPath) + + ctx, span := handler.Tracer().Start(parentCtx, "start-sqlite") + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + // create the directory to the store if it doesn't exist + err = os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("could not create sqlite store") + } + + logger.Warnf("database is at %s/synapse.db", dbPath) + + gdb, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s/%s", dbPath, "synapse.db")), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + Logger: common_base.GetGormLogger(logger), + FullSaveAssociations: true, + SkipDefaultTransaction: true, + }) + if err != nil { + return nil, fmt.Errorf("could not connect to db %s: %w", dbPath, err) + } + + handler.AddGormCallbacks(gdb) + + if !skipMigrations { + err = gdb.WithContext(ctx).AutoMigrate(base.GetAllModels()...) + if err != nil { + return nil, fmt.Errorf("could not migrate models: %w", err) + } + } + return &Store{base.NewStore(gdb, handler)}, nil +} + +var _ submitterDB.SubmitterDBFactory = &Store{} diff --git a/contrib/opbot/sql/store.go b/contrib/opbot/sql/store.go new file mode 100644 index 0000000000..a2d524b332 --- /dev/null +++ b/contrib/opbot/sql/store.go @@ -0,0 +1,36 @@ +package sql + +import ( + "context" + "errors" + "fmt" + "github.com/synapsecns/sanguine/contrib/opbot/sql/mysql" + "github.com/synapsecns/sanguine/contrib/opbot/sql/sqlite" + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" +) + +// Connect connects to the database. +func Connect(ctx context.Context, dbType dbcommon.DBType, path string, metrics metrics.Handler) (submitterDB.SubmitterDBFactory, error) { + switch dbType { + case dbcommon.Mysql: + store, err := mysql.NewMysqlStore(ctx, path, metrics) + if err != nil { + return nil, fmt.Errorf("could not create mysql store: %w", err) + } + + return store, nil + case dbcommon.Sqlite: + store, err := sqlite.NewSqliteStore(ctx, path, metrics, false) + if err != nil { + return nil, fmt.Errorf("could not create sqlite store: %w", err) + } + + return store, nil + case dbcommon.Clickhouse: + return nil, errors.New("driver not supported") + default: + return nil, fmt.Errorf("unsupported driver: %s", dbType) + } +} diff --git a/ethergo/submitter/db/service.go b/ethergo/submitter/db/service.go index b0d27960f4..ad2bbd78d2 100644 --- a/ethergo/submitter/db/service.go +++ b/ethergo/submitter/db/service.go @@ -47,6 +47,11 @@ type Service interface { // TransactionFunc is a function that can be passed to DBTransaction. type TransactionFunc func(ctx context.Context, svc Service) error +// SubmitterDBFactory is the interface for the tx queue database factory. +type SubmitterDBFactory interface { + SubmitterDB() Service +} + // Status is the status of a tx. // //go:generate go run golang.org/x/tools/cmd/stringer -type=Status -linecomment diff --git a/go.work.sum b/go.work.sum index 932e84c975..f5612891b6 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2402,8 +2402,6 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.4.1 h1:/7clKqrVfiVwiBQLM0Uke4KvXnO6JcCTS7HwF2D6wG8= github.com/gostaticanalysis/comment v1.4.1 h1:xHopR5L2lRz6OsjH4R2HG5wRhW9ySl3FsHIvi5pcXwc= github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5 h1:rx8127mFPqXXsfPSo8BwnIU97MKFZc89WHAHt8PwDVY= @@ -3336,6 +3334,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvq github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ= +github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/slack-io/commander v0.0.0-20231120025847-9fd78b4b2d54 h1:aRc+G2mUb697z6bR09Roq6kP08suJulgNo00SGhAsfM= github.com/slack-io/commander v0.0.0-20231120025847-9fd78b4b2d54/go.mod h1:aHmXZnL/ELKlfMybblXnnCl+IeHQ+gMb++DT7ujIGlA= github.com/slack-io/proper v0.0.0-20231119200853-f78ba4fc878f h1:wiEJBKJKvMOeE9KtLV7iwtHOsNXDBZloL+FPlkZ6vnA= diff --git a/services/cctp-relayer/db/relayer_db.go b/services/cctp-relayer/db/relayer_db.go index 0da5209f03..556568149d 100644 --- a/services/cctp-relayer/db/relayer_db.go +++ b/services/cctp-relayer/db/relayer_db.go @@ -2,11 +2,10 @@ package db import ( "context" + "github.com/synapsecns/sanguine/ethergo/submitter/db" "github.com/ethereum/go-ethereum/common" listenerDB "github.com/synapsecns/sanguine/ethergo/listener/db" - submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" - "github.com/synapsecns/sanguine/services/cctp-relayer/types" ) @@ -34,6 +33,6 @@ type CCTPRelayerDBWriter interface { type CCTPRelayerDB interface { CCTPRelayerDBReader CCTPRelayerDBWriter - SubmitterDB() submitterDB.Service + db.SubmitterDBFactory listenerDB.ChainListenerDB } diff --git a/services/rfq/relayer/relapi/client.go b/services/rfq/relayer/relapi/client.go index cd643cec4c..f00ecbe707 100644 --- a/services/rfq/relayer/relapi/client.go +++ b/services/rfq/relayer/relapi/client.go @@ -18,6 +18,7 @@ type RelayerClient interface { GetQuoteRequestStatusByTxID(ctx context.Context, hash string) (*GetQuoteRequestStatusResponse, error) RetryTransaction(ctx context.Context, txhash string) (*GetTxRetryResponse, error) Withdraw(ctx context.Context, req *WithdrawRequest) (*WithdrawResponse, error) + GetQuoteRequestByTXID(ctx context.Context, txid string) (*GetQuoteRequestResponse, error) } type relayerClient struct { @@ -122,3 +123,20 @@ func (r *relayerClient) Withdraw(ctx context.Context, req *WithdrawRequest) (*Wi return &res, nil } + +func (r *relayerClient) GetQuoteRequestByTXID(ctx context.Context, txid string) (*GetQuoteRequestResponse, error) { + var res GetQuoteRequestResponse + resp, err := r.client.R().SetContext(ctx). + SetQueryParam("id", txid). + SetResult(&res). + Get(getRequestByTxID) + if err != nil { + return nil, fmt.Errorf("failed to get quote request by tx id: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + + return &res, nil +} diff --git a/services/rfq/relayer/relapi/client_test.go b/services/rfq/relayer/relapi/client_test.go index 875eb71c04..d93e0a5def 100644 --- a/services/rfq/relayer/relapi/client_test.go +++ b/services/rfq/relayer/relapi/client_test.go @@ -1,6 +1,7 @@ package relapi_test import ( + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" ) @@ -49,3 +50,14 @@ func (c *RelayerClientSuite) TestRetryTransaction() { c.Equal(resp.TxID, hexutil.Encode(testReq.TransactionID[:])) } + +func (c *RelayerClientSuite) TestGetQuoteByTX() { + testReq := c.underlying.getTestQuoteRequest(reldb.Seen) + err := c.underlying.database.StoreQuoteRequest(c.GetTestContext(), testReq) + c.Require().NoError(err) + + resp, err := c.Client.GetQuoteRequestByTXID(c.GetTestContext(), hexutil.Encode(testReq.TransactionID[:])) + c.Require().NoError(err) + + c.Equal(len(common.Hex2Bytes(resp.QuoteRequestRaw)), len(testReq.RawRequest)) +} diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 9b5d741cbd..929be8aa40 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -142,6 +142,34 @@ func (h *Handler) GetTxRetry(c *gin.Context) { c.JSON(http.StatusOK, resp) } +func (h *Handler) GetQuoteRequestByTxID(c *gin.Context) { + txIDStr := c.Query("id") + if txIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'id'"}) + return + } + + txIDBytes, err := hexutil.Decode(txIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid txID"}) + return + } + var txID [32]byte + copy(txID[:], txIDBytes) + + quoteRequest, err := h.db.GetQuoteRequestByID(c, txID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := GetQuoteRequestResponse{ + QuoteRequestRaw: common.Bytes2Hex(quoteRequest.RawRequest), + } + c.JSON(http.StatusOK, resp) + +} + // Withdraw withdraws tokens from the relayer. // //nolint:cyclop diff --git a/services/rfq/relayer/relapi/model.go b/services/rfq/relayer/relapi/model.go index dbae73fd14..dffab9ce5c 100644 --- a/services/rfq/relayer/relapi/model.go +++ b/services/rfq/relayer/relapi/model.go @@ -24,3 +24,8 @@ type PutRelayAckResponse struct { ShouldRelay bool `json:"should_relay"` RelayerAddress string `json:"relayer_address"` } + +// GetQuoteRequestResponse is the response to a get quote request. +type GetQuoteRequestResponse struct { + QuoteRequestRaw string `json:"quote_request"` +} diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 709a6c3b12..f57c01e59c 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -101,6 +101,7 @@ const ( getQuoteStatusByTxIDRoute = "/status/by_tx_id" getRetryRoute = "/retry" postWithdrawRoute = "/withdraw" + getRequestByTxID = "/request/by_tx_id" ) var logger = log.Logger("relayer-api") @@ -117,6 +118,7 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { engine.GET(getQuoteStatusByTxHashRoute, h.GetQuoteRequestStatusByTxHash) engine.GET(getQuoteStatusByTxIDRoute, h.GetQuoteRequestStatusByTxID) engine.GET(getRetryRoute, h.GetTxRetry) + engine.GET(getRequestByTxID, h.GetQuoteRequestByTxID) engine.GET(metrics.MetricsPathDefault, gin.WrapH(r.handler.Handler())) if r.cfg.EnableAPIWithdrawals {