ethergo/submitter/db/service.go
package db
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/synapsecns/sanguine/core/dbcommon"
)
// Service is the interface for the tx queue database.
// note: the other files in this package (base, sqlite, mysql) provide a suggested implementation.
// you can implement these yourself. If you plan on importing them, you should wrap in your own service.
//
//go:generate go run github.com/vektra/mockery/v2 --name Service --output ./mocks --case=underscore
type Service interface {
// GetNonceForChainID gets the nonce for a given chain id.
GetNonceForChainID(ctx context.Context, fromAddress common.Address, chainID *big.Int) (nonce uint64, err error)
// PutTXS stores a tx in the database.
PutTXS(ctx context.Context, txs ...TX) error
// GetTXS gets all txs for a given address and chain id. If chain id is nil, it will get all txs for the address.
GetTXS(ctx context.Context, fromAddress common.Address, chainID *big.Int, options ...Option) (txs []TX, err error)
// MarkAllBeforeNonceReplacedOrConfirmed marks all txs for a given chain id and address before a given nonce as replaced or confirmed.
// TODO: cleaner function name
MarkAllBeforeNonceReplacedOrConfirmed(ctx context.Context, signer common.Address, chainID *big.Int, nonce uint64) error
// DBTransaction executes a transaction on the database.
// the function passed in will be passed a new service that is scoped to the transaction.
DBTransaction(ctx context.Context, f TransactionFunc) error
// GetAllTXAttemptByStatus gets all txs for a given address and chain id with a given status.
GetAllTXAttemptByStatus(ctx context.Context, fromAddress common.Address, chainID *big.Int, options ...Option) (txs []TX, err error)
// GetNonceStatus returns the nonce status for a given nonce by aggregating all attempts and finding the highest status.
GetNonceStatus(ctx context.Context, fromAddress common.Address, chainID *big.Int, nonce uint64) (status Status, err error)
// GetNonceAttemptsByStatus gets all txs for a given address and chain id with a given status and nonce.
GetNonceAttemptsByStatus(ctx context.Context, fromAddress common.Address, chainID *big.Int, nonce uint64, matchStatuses ...Status) (txs []TX, err error)
// GetChainIDsByStatus gets the distinct chain ids for a given address and status.
GetChainIDsByStatus(ctx context.Context, fromAddress common.Address, matchStatuses ...Status) (chainIDs []*big.Int, err error)
// DeleteTXS deletes txs that are older than a given duration.
DeleteTXS(ctx context.Context, maxAge time.Duration, matchStatuses ...Status) error
// GetDistinctChainIDs gets the distinct chain ids for all txs.
GetDistinctChainIDs(ctx context.Context) ([]*big.Int, error)
}
// Option is a type for specifying optional parameters.
type Option func(*options)
type options struct {
statuses []Status
maxResults int
}
var _ OptionsFetcher = (*options)(nil)
// OptionsFetcher is the interface for fetching options.
type OptionsFetcher interface {
Statuses() []Status
MaxResults() int
}
func (o *options) MaxResults() int {
return o.maxResults
}
func (o *options) Statuses() []Status {
return o.statuses
}
// DefaultMaxResultsPerChain is the maximum number of transactions to return per chain id.
// it is exported for testing.
// TODO: this should be an option passed to the GetTXs function.
// TODO: temporarily reduced from 50 to 1 to increase resiliency.
const DefaultMaxResultsPerChain = 10
// ParseOptions parses the options.
func ParseOptions(opts ...Option) OptionsFetcher {
myOptions := &options{
statuses: nil,
maxResults: DefaultMaxResultsPerChain, // Default to 0 for no limit.
}
for _, opt := range opts {
opt(myOptions)
}
return myOptions
}
// WithStatuses specifies the statuses to match.
func WithStatuses(statuses ...Status) Option {
return func(opts *options) {
opts.statuses = statuses
}
}
// WithMaxResults specifies the maximum number of results to return.
func WithMaxResults(maxResults int) Option {
return func(opts *options) {
opts.maxResults = maxResults
}
}
// 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
type Status uint8
// Important: do not modify the order of these constants.
// if one needs to be removed, replace it with a no-op status.
// additionally, due to the GetMaxNoncestatus function, statuses are currently assumed to be in order.
// if you need to modify this functionality, please update that function. to reflect that the highest status
// isno longer the expected end status.
const (
// Pending is the status of a tx that has not been processed yet.
Pending Status = iota + 1 // Pending
// Stored is the status of a tx that has been stored.
Stored // Stored
// Submitted is the status of a tx that has been submitted.
Submitted // Submitted
// FailedSubmit is the status of a tx that has failed to submit.
FailedSubmit // Failed
// ReplacedOrConfirmed is the status of a tx that has been replaced by a new tx or confirmed. The actual status will be set later.
ReplacedOrConfirmed // ReplacedOrConfirmed
// Replaced is the status of a tx that has been replaced by a new tx.
Replaced // Replaced
// Confirmed is the status of a tx that has been confirmed.
Confirmed // Confirmed
)
var allStatusTypes = []Status{Pending, Stored, Submitted, FailedSubmit, ReplacedOrConfirmed, Replaced, Confirmed}
// AllStatusTypes returns all status types.
// it is exported for testing purposes
//
// These are guaranteed to be in order.
func AllStatusTypes() []Status {
return allStatusTypes
}
// check to make sure all statuses are included in all status types.
func _() {
for i := 0; i < len(_Status_index); i++ {
statusNum := i + 1
status := Status(statusNum)
if status.String() == "" {
panic(fmt.Sprintf("invalid status: %d", status))
}
if status.String() != AllStatusTypes()[i].String() {
panic(fmt.Sprintf("status string and all status types do not match: %s, %s", status.String(), AllStatusTypes()[i]))
}
}
}
// Int returns the uint8 representation of the status.
func (s Status) Int() uint8 {
return uint8(s)
}
// GormDataType returns the gorm data type for the status.
func (s Status) GormDataType() string {
return dbcommon.EnumDataType
}
// Scan implements the gorm Scanner interface.
func (s *Status) Scan(src interface{}) error {
res, err := dbcommon.EnumScan(src)
if err != nil {
return fmt.Errorf("could not scan status: %w", err)
}
newStatus := Status(res)
*s = newStatus
found := false
for _, status := range allStatusTypes {
if status == *s {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid status: %d", res)
}
return nil
}
// Value implements the gorm Valuer interface.
func (s *Status) Value() (driver.Value, error) {
//nolint: wrapcheck
return dbcommon.EnumValue(s)
}
var _ dbcommon.EnumInter = (*Status)(nil)
var (
// ErrNoNonceForChain is the error returned when there is no nonce for a given chain id.
ErrNoNonceForChain = errors.New("no nonce exists for this chain")
// ErrNonceNotExist is the error returned when a nonce does not exist.
ErrNonceNotExist = errors.New("nonce does not exist")
)