services/rfq/api/db/api_db.go
// Package db provides the database interfaces and types for the RFQ API.
package db
import (
"context"
"database/sql/driver"
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/synapsecns/sanguine/core/dbcommon"
"github.com/synapsecns/sanguine/services/rfq/api/model"
)
// Quote is the database model for a quote.
type Quote struct {
// OriginChainID is the chain which the relayer is willing to relay from
OriginChainID uint64 `gorm:"column:origin_chain_id;index;primaryKey"`
// OriginTokenAddr is the token address for which the relayer willing to relay from
OriginTokenAddr string `gorm:"column:origin_token;index;primaryKey"`
// DestChainID is the chain which the relayer is willing to relay to
DestChainID uint64 `gorm:"column:dest_chain_id;index;primaryKey"`
// DestToken is the token address for which the relayer willing to relay to
DestTokenAddr string `gorm:"column:dest_token;index;primaryKey"`
// DestAmount is the max amount of liquidity which exists for a given destination token, provided in the destination token decimals
DestAmount decimal.Decimal `gorm:"column:dest_amount"`
// MaxOriginAmount is the maximum amount of origin tokens bridgeable
MaxOriginAmount decimal.Decimal `gorm:"column:max_origin_amount"`
// FixedFee is the fixed fee for the quote, provided in the destination token terms
FixedFee decimal.Decimal `gorm:"column:fixed_fee"`
// Address of the relayer providing the quote
RelayerAddr string `gorm:"column:relayer_address;primaryKey"`
// OriginFastBridgeAddress is the address of the fast bridge contract on the origin chain
OriginFastBridgeAddress string `gorm:"column:origin_fast_bridge_address"`
// DestFastBridgeAddress is the address of the fast bridge contract on the destination chain
DestFastBridgeAddress string `gorm:"column:dest_fast_bridge_address"`
// UpdatedAt is the time that the quote was last upserted
UpdatedAt time.Time
}
// ActiveQuoteRequestStatus is the status of a quote request in the db.
// This is the primary mechanism for moving data through the app.
//
// TODO: consider making this an interface and exporting that.
//
// EXTREMELY IMPORTANT: DO NOT ADD NEW VALUES TO THIS ENUM UNLESS THEY ARE AT THE END.
//
//go:generate go run golang.org/x/tools/cmd/stringer -type=ActiveQuoteRequestStatus
type ActiveQuoteRequestStatus uint8
const (
// Received means the quote request has been received by the server.
Received ActiveQuoteRequestStatus = iota + 1
// Pending means the quote request is pending awaiting relayer responses.
Pending
// Expired means the quote request has expired without any valid responses.
Expired
// Closed means the quote request has been fulfilled.
Closed
)
// Int returns the int value of the quote request status.
func (q ActiveQuoteRequestStatus) Int() uint8 {
return uint8(q)
}
// GormDataType implements the gorm common interface for enums.
func (q ActiveQuoteRequestStatus) GormDataType() string {
return dbcommon.EnumDataType
}
// Scan implements the gorm common interface for enums.
func (q *ActiveQuoteRequestStatus) Scan(src any) error {
res, err := dbcommon.EnumScan(src)
if err != nil {
return fmt.Errorf("could not scan %w", err)
}
newStatus := ActiveQuoteRequestStatus(res)
*q = newStatus
return nil
}
// Value implements the gorm common interface for enums.
func (q ActiveQuoteRequestStatus) Value() (driver.Value, error) {
// nolint: wrapcheck
return dbcommon.EnumValue(q)
}
var _ dbcommon.Enum = (*ActiveQuoteRequestStatus)(nil)
// ActiveQuoteResponseStatus is the status of a quote request in the db.
// This is the primary mechanism for moving data through the app.
//
// TODO: consider making this an interface and exporting that.
//
// EXTREMELY IMPORTANT: DO NOT ADD NEW VALUES TO THIS ENUM UNLESS THEY ARE AT THE END.
//
//go:generate go run golang.org/x/tools/cmd/stringer -type=ActiveQuoteResponseStatus
type ActiveQuoteResponseStatus uint8
const (
// Considered means the quote request was considered by the relayer, but was not ultimately the fulfilling response.
Considered ActiveQuoteResponseStatus = iota + 1
// Returned means the quote request was returned by the relayer to the user.
Returned
// PastExpiration means the quote request was received, but past the expiration window.
PastExpiration
// Malformed means that the quote request was malformed.
Malformed
// Duplicate means that the quote request was a duplicate.
Duplicate
)
// Int returns the int value of the quote request status.
func (q ActiveQuoteResponseStatus) Int() uint8 {
return uint8(q)
}
// GormDataType implements the gorm common interface for enums.
func (q ActiveQuoteResponseStatus) GormDataType() string {
return dbcommon.EnumDataType
}
// Scan implements the gorm common interface for enums.
func (q *ActiveQuoteResponseStatus) Scan(src any) error {
res, err := dbcommon.EnumScan(src)
if err != nil {
return fmt.Errorf("could not scan %w", err)
}
newStatus := ActiveQuoteResponseStatus(res)
*q = newStatus
return nil
}
// Value implements the gorm common interface for enums.
func (q ActiveQuoteResponseStatus) Value() (driver.Value, error) {
// nolint: wrapcheck
return dbcommon.EnumValue(q)
}
var _ dbcommon.Enum = (*ActiveQuoteResponseStatus)(nil)
// ActiveQuoteRequest is the database model for an active quote request.
type ActiveQuoteRequest struct {
RequestID string `gorm:"column:request_id;primaryKey"`
IntegratorID string `gorm:"column:integrator_id"`
UserAddress string `gorm:"column:user_address"`
OriginChainID uint64 `gorm:"column:origin_chain_id"`
OriginTokenAddr string `gorm:"column:origin_token"`
DestChainID uint64 `gorm:"column:dest_chain_id"`
DestTokenAddr string `gorm:"column:dest_token"`
OriginAmountExact decimal.Decimal `gorm:"column:origin_amount_exact"`
ExpirationWindow time.Duration `gorm:"column:expiration_window"`
CreatedAt time.Time `gorm:"column:created_at"`
Status ActiveQuoteRequestStatus `gorm:"column:status"`
ClosedAt *time.Time `gorm:"column:closed_at"`
ClosedQuoteID *string `gorm:"column:closed_quote_id"`
}
// FromUserRequest converts a model.PutRFQRequest to an ActiveQuoteRequest.
func FromUserRequest(req *model.PutRFQRequest, requestID string) (*ActiveQuoteRequest, error) {
originAmountExact, err := decimal.NewFromString(req.Data.OriginAmountExact)
if err != nil {
return nil, fmt.Errorf("invalid origin amount: %w", err)
}
return &ActiveQuoteRequest{
RequestID: requestID,
IntegratorID: req.IntegratorID,
UserAddress: req.UserAddress,
OriginChainID: uint64(req.Data.OriginChainID), //nolint:gosec
OriginTokenAddr: req.Data.OriginTokenAddr,
DestChainID: uint64(req.Data.DestChainID), //nolint:gosec
DestTokenAddr: req.Data.DestTokenAddr,
OriginAmountExact: originAmountExact,
ExpirationWindow: time.Duration(req.Data.ExpirationWindow),
CreatedAt: time.Now(),
Status: Received,
}, nil
}
// ActiveQuoteResponse is the database model for an active quote response.
type ActiveQuoteResponse struct {
RequestID string `gorm:"column:request_id"`
QuoteID string `gorm:"column:quote_id;primaryKey"`
DestAmount decimal.Decimal `gorm:"column:dest_amount"`
RelayerAddr string `gorm:"column:relayer_address"`
UpdatedAt time.Time `gorm:"column:updated_at"`
Status ActiveQuoteResponseStatus `gorm:"column:status"`
}
// FromRelayerResponse converts a model.WsRFQResponse to an ActiveQuoteResponse.
func FromRelayerResponse(resp *model.WsRFQResponse, relayerAddr string, status ActiveQuoteResponseStatus) (*ActiveQuoteResponse, error) {
destAmount, err := decimal.NewFromString(resp.DestAmount)
if err != nil {
return nil, fmt.Errorf("invalid dest amount: %w", err)
}
return &ActiveQuoteResponse{
RequestID: resp.RequestID,
QuoteID: resp.QuoteID,
DestAmount: destAmount,
RelayerAddr: relayerAddr,
UpdatedAt: resp.UpdatedAt,
Status: status,
}, nil
}
// APIDBReader is the interface for reading from the database.
type APIDBReader interface {
// GetQuotesByDestChainAndToken gets quotes from the database by destination chain and token.
GetQuotesByDestChainAndToken(ctx context.Context, destChainID uint64, destTokenAddr string) ([]*Quote, error)
// GetQuotesByOriginAndDestination gets quotes from the database by origin and destination.
GetQuotesByOriginAndDestination(ctx context.Context, originChainID uint64, originTokenAddr string, destChainID uint64, destTokenAddr string) ([]*Quote, error)
// GetQuotesByRelayerAddress gets quotes from the database by relayer address.
GetQuotesByRelayerAddress(ctx context.Context, relayerAddress string) ([]*Quote, error)
// GetActiveQuoteRequests gets active quote requests from the database.
GetActiveQuoteRequests(ctx context.Context, matchStatuses ...ActiveQuoteRequestStatus) ([]*ActiveQuoteRequest, error)
// GetAllQuotes retrieves all quotes from the database.
GetAllQuotes(ctx context.Context) ([]*Quote, error)
}
// APIDBWriter is the interface for writing to the database.
type APIDBWriter interface {
// UpsertQuote upserts a quote in the database.
UpsertQuote(ctx context.Context, quote *Quote) error
// UpsertQuotes upserts multiple quotes in the database.
UpsertQuotes(ctx context.Context, quotes []*Quote) error
// InsertActiveQuoteRequest inserts an active quote request into the database.
InsertActiveQuoteRequest(ctx context.Context, req *model.PutRFQRequest, requestID string) error
// UpdateActiveQuoteRequestStatus updates the status of an active quote request in the database.
UpdateActiveQuoteRequestStatus(ctx context.Context, requestID string, quoteID *string, status ActiveQuoteRequestStatus) error
// InsertActiveQuoteResponse inserts an active quote response into the database.
InsertActiveQuoteResponse(ctx context.Context, resp *model.WsRFQResponse, relayerAddr string, status ActiveQuoteResponseStatus) error
// UpdateActiveQuoteResponseStatus updates the status of an active quote response in the database.
UpdateActiveQuoteResponseStatus(ctx context.Context, quoteID string, status ActiveQuoteResponseStatus) error
}
// APIDB is the interface for the database service.
type APIDB interface {
APIDBReader
APIDBWriter
}