Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFQ API: add GET /ack endpoint #2643

Merged
merged 24 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions services/rfq/api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
GetAllQuotes(ctx context.Context) ([]*model.GetQuoteResponse, error)
GetSpecificQuote(ctx context.Context, q *model.GetQuoteSpecificRequest) ([]*model.GetQuoteResponse, error)
GetQuoteByRelayerAddress(ctx context.Context, relayerAddr string) ([]*model.GetQuoteResponse, error)
GetRelayAck(ctx context.Context, txID string) (*model.GetRelayAckResponse, error)
resty() *resty.Client
}

Expand Down Expand Up @@ -188,3 +189,24 @@

return quotes, nil
}

func (c *unauthenticatedClient) GetRelayAck(ctx context.Context, txID string) (*model.GetRelayAckResponse, error) {
var ack *model.GetRelayAckResponse
resp, err := c.rClient.R().
SetContext(ctx).
SetQueryParams(map[string]string{
"id": txID,
}).
SetResult(&ack).
Get(rest.AckRoute)

if err != nil {
return nil, fmt.Errorf("error from server: %s %w", resp.Status(), err)
}

Check warning on line 205 in services/rfq/api/client/client.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/api/client/client.go#L193-L205

Added lines #L193 - L205 were not covered by tests

if resp.IsError() {
return nil, fmt.Errorf("error from server: %s", resp.Status())
}

Check warning on line 209 in services/rfq/api/client/client.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/api/client/client.go#L207-L209

Added lines #L207 - L209 were not covered by tests

return ack, nil

Check warning on line 211 in services/rfq/api/client/client.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/api/client/client.go#L211

Added line #L211 was not covered by tests
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling for clarity in the GetRelayAck method.

-	if err != nil {
-		return nil, fmt.Errorf("error from server: %s %w", resp.Status(), err)
-	}
-
-	if resp.IsError() {
-		return nil, fmt.Errorf("error from server: %s", resp.Status())
-	}
+	if err != nil || resp.IsError() {
+		return nil, fmt.Errorf("error from server: %s %w", resp.Status(), err)
+	}

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
func (c *unauthenticatedClient) GetRelayAck(ctx context.Context, txID string) (*model.GetRelayAckResponse, error) {
var ack *model.GetRelayAckResponse
resp, err := c.rClient.R().
SetContext(ctx).
SetQueryParams(map[string]string{
"id": txID,
}).
SetResult(&ack).
Get(rest.AckRoute)
if err != nil {
return nil, fmt.Errorf("error from server: %s %w", resp.Status(), err)
}
if resp.IsError() {
return nil, fmt.Errorf("error from server: %s", resp.Status())
}
return ack, nil
}
func (c *unauthenticatedClient) GetRelayAck(ctx context.Context, txID string) (*model.GetRelayAckResponse, error) {
var ack *model.GetRelayAckResponse
resp, err := c.rClient.R().
SetContext(ctx).
SetQueryParams(map[string]string{
"id": txID,
}).
SetResult(&ack).
Get(rest.AckRoute)
if err != nil || resp.IsError() {
return nil, fmt.Errorf("error from server: %s %w", resp.Status(), err)
}
return ack, nil
}

16 changes: 14 additions & 2 deletions services/rfq/api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/jftuga/ellipsis"
"gopkg.in/yaml.v2"
Expand All @@ -21,8 +22,19 @@ type Config struct {
Database DatabaseConfig `yaml:"database"`
OmniRPCURL string `yaml:"omnirpc_url"`
// bridges is a map of chainid->address
Bridges map[uint32]string `yaml:"bridges"`
Port string `yaml:"port"`
Bridges map[uint32]string `yaml:"bridges"`
Port string `yaml:"port"`
RelayAckTimeout time.Duration `yaml:"relay_ack_timeout"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a YAML tag for RelayAckTimeout.

- RelayAckTimeout time.Duration     `yaml:"relay_ack_timeout"`
+ RelayAckTimeout time.Duration     `yaml:"relay_ack_timeout" yaml:"relay_ack_timeout"`

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
RelayAckTimeout time.Duration `yaml:"relay_ack_timeout"`
RelayAckTimeout time.Duration `yaml:"relay_ack_timeout" yaml:"relay_ack_timeout"`

}

const defaultRelayAckTimeout = 5 * time.Second
dwasse marked this conversation as resolved.
Show resolved Hide resolved

// GetRelayAckTimeout returns the relay ack timeout.
func (c Config) GetRelayAckTimeout() time.Duration {
if c.RelayAckTimeout == 0 {
return defaultRelayAckTimeout
}
return c.RelayAckTimeout
}

// LoadConfig loads the config from the given path.
Expand Down
8 changes: 8 additions & 0 deletions services/rfq/api/model/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ type GetQuoteResponse struct {
// UpdatedAt is the time that the quote was last upserted
UpdatedAt string `json:"updated_at"`
}

// GetRelayAckResponse contains the schema for a GET /relay/ack response.
type GetRelayAckResponse struct {
dwasse marked this conversation as resolved.
Show resolved Hide resolved
// TxID is the transaction ID
TransactionID string `json:"tx_id"`
// ShouldRelay is a boolean indicating whether the transaction should be relayed
ShouldRelay bool `json:"should_relay"`
}
48 changes: 47 additions & 1 deletion services/rfq/api/rest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"context"
"fmt"
"net/http"
"sync"
"time"

"github.com/ipfs/go-log"
Expand All @@ -25,6 +26,7 @@
"github.com/synapsecns/sanguine/services/rfq/api/docs"
"github.com/synapsecns/sanguine/services/rfq/api/model"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge"
"github.com/synapsecns/sanguine/services/rfq/relayer/relapi"
)

// QuoterAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts.
Expand All @@ -37,6 +39,11 @@
handler metrics.Handler
fastBridgeContracts map[uint32]*fastbridge.FastBridge
roleCache map[uint32]*ttlcache.Cache[string, bool]
// relayAckCache contains a set of transactionID values that reflect
// transactions that have been acked for relay
relayAckCache *ttlcache.Cache[string, bool]
// ackMux is a mutex used to ensure that only one transaction id can be acked at a time.
ackMux sync.Mutex
}

// NewAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts.
Expand Down Expand Up @@ -80,27 +87,40 @@
ttlcache.WithTTL[string, bool](cacheInterval),
)
roleCache := roles[chainID]

go roleCache.Start()
go func() {
<-ctx.Done()
roleCache.Stop()
}()
}

// create the relay ack cache
relayAckCache := ttlcache.New[string, bool](
dwasse marked this conversation as resolved.
Show resolved Hide resolved
ttlcache.WithTTL[string, bool](cfg.GetRelayAckTimeout()),
ttlcache.WithDisableTouchOnHit[string, bool](),
)
go relayAckCache.Start()
go func() {
<-ctx.Done()
relayAckCache.Stop()
}()

return &QuoterAPIServer{
cfg: cfg,
db: store,
omnirpcClient: omniRPCClient,
handler: handler,
fastBridgeContracts: bridges,
roleCache: roles,
relayAckCache: relayAckCache,
ackMux: sync.Mutex{},
}, nil
}

// QuoteRoute is the API endpoint for handling quote related requests.
const (
QuoteRoute = "/quotes"
AckRoute = "/ack"
cacheInterval = time.Minute
)

Expand All @@ -120,6 +140,7 @@
// GET routes without the AuthMiddleware
// engine.PUT("/quotes", h.ModifyQuote)
engine.GET(QuoteRoute, h.GetQuotes)
engine.GET(AckRoute, r.GetRelayAck)
dwasse marked this conversation as resolved.
Show resolved Hide resolved

r.engine = engine

Expand Down Expand Up @@ -188,3 +209,28 @@
c.Next()
}
}

// GetRelayAck checks if a relay is pending or not.
func (r *QuoterAPIServer) GetRelayAck(c *gin.Context) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this w/ swagger docs? You should see this in handler.go and be able to regenerate. Just consists of copying the comments over from anything in handler.go and rerunning go generate followed by a yarn docusaurus gen-api-docs all from docs/bridge

Would also be good to add the endpoint here

and document the step here

transactionID := c.Query("id")
if transactionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'id'"})
return
}

Check warning on line 219 in services/rfq/api/rest/server.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/api/rest/server.go#L217-L219

Added lines #L217 - L219 were not covered by tests

// If the tx id is already in the cache, it should not be relayed.
// Otherwise, insert into the cache.
r.ackMux.Lock()
ack := r.relayAckCache.Get(transactionID)
shouldRelay := ack == nil
if shouldRelay {
r.relayAckCache.Set(transactionID, true, ttlcache.DefaultTTL)
}
r.ackMux.Unlock()

resp := relapi.GetRelayAckResponse{
TxID: transactionID,
ShouldRelay: shouldRelay,
}
c.JSON(http.StatusOK, resp)
}
45 changes: 45 additions & 0 deletions services/rfq/api/rest/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/synapsecns/sanguine/ethergo/signer/wallet"
"github.com/synapsecns/sanguine/services/rfq/api/model"
"github.com/synapsecns/sanguine/services/rfq/relayer/relapi"
)

func (c *ServerSuite) TestNewQuoterAPIServer() {
Expand Down Expand Up @@ -203,6 +204,50 @@ func (c *ServerSuite) TestPutAndGetQuoteByRelayer() {
c.Assert().True(found, "Newly added quote not found")
}

func (c *ServerSuite) TestGetAck() {
c.startQuoterAPIServer()

// Send GET request
client := &http.Client{}
testTxID := "0x123"
req, err := http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/ack?id=%s", c.port, testTxID), nil)
c.Require().NoError(err)
resp, err := client.Do(req)
c.Require().NoError(err)
c.Equal(http.StatusOK, resp.StatusCode)

// Expect ack with shouldRelay=true
var result relapi.GetRelayAckResponse
err = json.NewDecoder(resp.Body).Decode(&result)
c.Require().NoError(err)
expectedResult := relapi.GetRelayAckResponse{
TxID: testTxID,
ShouldRelay: true,
}
c.Equal(expectedResult, result)
err = resp.Body.Close()
c.Require().NoError(err)

// Send another request with same txID
req, err = http.NewRequestWithContext(c.GetTestContext(), http.MethodGet, fmt.Sprintf("http://localhost:%d/ack?id=%s", c.port, testTxID), nil)
c.Require().NoError(err)
resp, err = client.Do(req)
c.Require().NoError(err)
c.Equal(http.StatusOK, resp.StatusCode)

// Expect ack with shouldRelay=false
err = json.NewDecoder(resp.Body).Decode(&result)
c.Require().NoError(err)
expectedResult = relapi.GetRelayAckResponse{
TxID: testTxID,
ShouldRelay: false,
}
c.Equal(expectedResult, result)
err = resp.Body.Close()
c.Require().NoError(err)
c.GetTestContext().Done()
}

// startQuoterAPIServer starts the API server and waits for it to initialize.
func (c *ServerSuite) startQuoterAPIServer() {
go func() {
Expand Down
2 changes: 1 addition & 1 deletion services/rfq/relayer/relapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
func (h *Handler) GetQuoteRequestStatusByTxID(c *gin.Context) {
txIDStr := c.Query("id")
if txIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'txID'"})
c.JSON(http.StatusBadRequest, gin.H{"error": "Must specify 'id'"})

Check warning on line 63 in services/rfq/relayer/relapi/handler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relapi/handler.go#L63

Added line #L63 was not covered by tests
return
}

Expand Down
6 changes: 6 additions & 0 deletions services/rfq/relayer/relapi/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ type GetTxRetryResponse struct {
Nonce uint64 `json:"nonce"`
GasAmount string `json:"gas_amount"`
}

// GetRelayAckResponse contains the schema for a POST /relay/ack response.
type GetRelayAckResponse struct {
TxID string `json:"tx_id"`
ShouldRelay bool `json:"should_relay"`
}
13 changes: 12 additions & 1 deletion services/rfq/relayer/service/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,25 @@
return fmt.Errorf("could not get committable balance: %w", err)
}

// if committableBalance > destAmount
// check if we have enough inventory to handle the request
if committableBalance.Cmp(request.Transaction.DestAmount) < 0 {
err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.NotEnoughInventory)
if err != nil {
return fmt.Errorf("could not update request status: %w", err)
}
return nil
}

Comment on lines +142 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add logging for better traceability and debugging.

// check if we have enough inventory to handle the request
if committableBalance.Cmp(request.Transaction.DestAmount) < 0 {
    logger.Infof("Not enough inventory for transaction ID: %s", hexutil.Encode(request.TransactionID[:]))
    err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.NotEnoughInventory)
    if err != nil {
        return fmt.Errorf("could not update request status: %w", err)
    }
    return nil
}

// get ack from API to synchronize calls with other relayers and avoid reverts
resp, err := q.apiClient.GetRelayAck(ctx, hexutil.Encode(request.TransactionID[:]))
if err != nil {
return fmt.Errorf("could not get relay ack: %w", err)
}
if !resp.ShouldRelay {
span.SetAttributes(attribute.Bool("should_relay", false))
return nil
}

Check warning on line 158 in services/rfq/relayer/service/handlers.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/handlers.go#L151-L158

Added lines #L151 - L158 were not covered by tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper error handling and logging for API call to GetRelayAck.

The added code correctly calls the GetRelayAck API and handles the response. However, it would be beneficial to add logging for better traceability and debugging.

// get ack from API to synchronize calls with other relayers and avoid reverts
resp, err := q.apiClient.GetRelayAck(ctx, hexutil.Encode(request.TransactionID[:]))
if err != nil {
    logger.Errorf("Failed to get relay ack for transaction ID: %s, error: %v", hexutil.Encode(request.TransactionID[:]), err)
    return fmt.Errorf("could not get relay ack: %w", err)
}
if !resp.ShouldRelay {
    span.SetAttributes(attribute.Bool("should_relay", false))
    logger.Infof("Relay not required for transaction ID: %s", hexutil.Encode(request.TransactionID[:]))
    return nil
}

err = q.db.UpdateQuoteRequestStatus(ctx, request.TransactionID, reldb.CommittedPending)
if err != nil {
return fmt.Errorf("could not update request status: %w", err)
Expand Down
8 changes: 8 additions & 0 deletions services/rfq/relayer/service/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
cctpSql "github.com/synapsecns/sanguine/services/cctp-relayer/db/sql"
"github.com/synapsecns/sanguine/services/cctp-relayer/relayer"
omniClient "github.com/synapsecns/sanguine/services/omnirpc/client"
rfqAPIClient "github.com/synapsecns/sanguine/services/rfq/api/client"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge"
"github.com/synapsecns/sanguine/services/rfq/relayer/inventory"
"github.com/synapsecns/sanguine/services/rfq/relayer/pricer"
Expand All @@ -40,6 +41,7 @@
client omniClient.RPCClient
chainListeners map[int]listener.ContractListener
apiServer *relapi.RelayerAPIServer
apiClient rfqAPIClient.UnauthenticatedClient
inventory inventory.Manager
quoter quoter.Quoter
submitter submitter.TransactionSubmitter
Expand Down Expand Up @@ -120,6 +122,11 @@
return nil, fmt.Errorf("could not get api server: %w", err)
}

apiClient, err := rfqAPIClient.NewUnauthenticatedClient(metricHandler, cfg.GetRfqAPIURL())
dwasse marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("error creating RFQ API client: %w", err)
}

Check warning on line 128 in services/rfq/relayer/service/relayer.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/relayer.go#L125-L128

Added lines #L125 - L128 were not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve the error message for consistency in the NewRelayer function.

-	return nil, fmt.Errorf("error creating RFQ API client: %w", err)
+	return nil, fmt.Errorf("could not create RFQ API client: %w", err)

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
apiClient, err := rfqAPIClient.NewUnauthenticatedClient(metricHandler, cfg.GetRfqAPIURL())
if err != nil {
return nil, fmt.Errorf("error creating RFQ API client: %w", err)
}
apiClient, err := rfqAPIClient.NewUnauthenticatedClient(metricHandler, cfg.GetRfqAPIURL())
if err != nil {
return nil, fmt.Errorf("could not create RFQ API client: %w", err)
}


cache := ttlcache.New[common.Hash, bool](ttlcache.WithTTL[common.Hash, bool](time.Second * 30))
rel := Relayer{
db: store,
Expand All @@ -134,6 +141,7 @@
signer: sg,
chainListeners: chainListeners,
apiServer: apiServer,
apiClient: apiClient,

Check warning on line 144 in services/rfq/relayer/service/relayer.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/relayer.go#L144

Added line #L144 was not covered by tests
}
return &rel, nil
}
Expand Down
4 changes: 4 additions & 0 deletions services/rfq/relayer/service/statushandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/jellydator/ttlcache/v3"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/services/rfq/api/client"
"github.com/synapsecns/sanguine/services/rfq/relayer/chain"
"github.com/synapsecns/sanguine/services/rfq/relayer/inventory"
"github.com/synapsecns/sanguine/services/rfq/relayer/quoter"
Expand Down Expand Up @@ -42,6 +43,8 @@
RelayerAddress common.Address
// metrics is the metrics handler.
metrics metrics.Handler
// apiClient is used to get acks before submitting a relay transaction.
apiClient client.UnauthenticatedClient
}

// Handler is the handler for a quote request.
Expand All @@ -68,6 +71,7 @@
metrics: r.metrics,
RelayerAddress: r.signer.Address(),
claimCache: r.claimCache,
apiClient: r.apiClient,

Check warning on line 74 in services/rfq/relayer/service/statushandler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/statushandler.go#L74

Added line #L74 was not covered by tests
}

qr.handlers[reldb.Seen] = r.deadlineMiddleware(r.gasMiddleware(qr.handleSeen))
Expand Down
Loading