services/rfq/relayer/relapi/suite_test.go
package relapi_test
import (
"fmt"
"math/big"
"strconv"
"sync"
"testing"
"github.com/ethereum/go-ethereum/params"
"github.com/synapsecns/sanguine/services/rfq/testutil"
"github.com/synapsecns/sanguine/services/rfq/util"
"github.com/Flaque/filet"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/phayes/freeport"
"github.com/puzpuzpuz/xsync/v2"
"github.com/stretchr/testify/suite"
"github.com/synapsecns/sanguine/core/dbcommon"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/core/testsuite"
"github.com/synapsecns/sanguine/ethergo/backends"
"github.com/synapsecns/sanguine/ethergo/backends/geth"
"github.com/synapsecns/sanguine/ethergo/signer/signer/localsigner"
"github.com/synapsecns/sanguine/ethergo/signer/wallet"
"github.com/synapsecns/sanguine/ethergo/submitter"
submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config"
omniClient "github.com/synapsecns/sanguine/services/omnirpc/client"
omnirpcHelper "github.com/synapsecns/sanguine/services/omnirpc/testhelper"
"github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge"
"github.com/synapsecns/sanguine/services/rfq/relayer/relapi"
"github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
"github.com/synapsecns/sanguine/services/rfq/relayer/reldb"
"github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect"
"golang.org/x/sync/errgroup"
)
// RelayerServer suite is the relayer API server test suite.
type RelayerServerSuite struct {
*testsuite.TestSuite
deployManager *testutil.DeployManager
omniRPCClient omniClient.RPCClient
omniRPCTestBackends []backends.SimulatedTestBackend
testBackendMux sync.Mutex
testBackends map[uint64]backends.SimulatedTestBackend
originChainID uint32
destChainID uint32
fastBridgeAddressMap *xsync.MapOf[uint64, common.Address]
database reldb.Service
cfg relconfig.Config
testWallet wallet.Wallet
handler metrics.Handler
RelayerAPIServer *relapi.RelayerAPIServer
port uint16
wallet wallet.Wallet
}
var testWithdrawalAddress = common.BigToAddress(big.NewInt(1))
// NewRelayerServerSuite creates a end-to-end test suite.
func NewRelayerServerSuite(tb testing.TB) *RelayerServerSuite {
tb.Helper()
return &RelayerServerSuite{
TestSuite: testsuite.NewTestSuite(tb),
}
}
func (c *RelayerServerSuite) SetupTest() {
c.TestSuite.SetupTest()
testOmnirpc := omnirpcHelper.NewOmnirpcServer(c.GetTestContext(), c.T(), c.omniRPCTestBackends...)
omniRPCClient := omniClient.NewOmnirpcClient(testOmnirpc, c.handler, omniClient.WithCaptureReqRes())
c.omniRPCClient = omniRPCClient
arbFastBridgeAddress, ok := c.fastBridgeAddressMap.Load(42161)
c.True(ok)
ethFastBridgeAddress, ok := c.fastBridgeAddressMap.Load(1)
c.True(ok)
port, err := freeport.GetFreePort()
c.port = uint16(port)
c.Require().NoError(err)
c.originChainID = 1
c.destChainID = 42161
testConfig := relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
int(c.originChainID): {
RFQAddress: ethFastBridgeAddress.Hex(),
},
int(c.destChainID): {
RFQAddress: arbFastBridgeAddress.Hex(),
},
},
RelayerAPIPort: strconv.Itoa(port),
Database: relconfig.DatabaseConfig{
Type: "sqlite",
DSN: filet.TmpFile(c.T(), "", "").Name(),
},
EnableAPIWithdrawals: true,
WithdrawalWhitelist: []string{
testWithdrawalAddress.String(),
},
QuotableTokens: map[string][]string{
// gas tokens.
fmt.Sprintf("%d-%s", c.originChainID, util.EthAddress): {
// not used for this test
},
fmt.Sprintf("%d-%s", c.destChainID, util.EthAddress): {
// not used for this test
},
c.getMockTokenID(c.testBackends[uint64(c.originChainID)]): {
// not used for this test
},
c.getMockTokenID(c.testBackends[uint64(c.destChainID)]): {
// not used for this test
},
},
}
c.cfg = testConfig
c.wallet, err = wallet.FromRandom()
c.Require().NoError(err)
signer := localsigner.NewSigner(c.wallet.PrivateKey())
submitterCfg := &submitterConfig.Config{}
ts := submitter.NewTransactionSubmitter(c.handler, signer, omniRPCClient, c.database.SubmitterDB(), submitterCfg)
var wg sync.WaitGroup
wg.Add(len(c.testBackends) * 2)
go func() {
// small potential for a race condition if submitter hasn't started by the time our test starts
_ = ts.Start(c.GetTestContext())
}()
for _, backend := range c.testBackends {
go func() {
defer wg.Done()
backend.FundAccount(c.GetTestContext(), c.wallet.Address(), *big.NewInt(params.Ether))
}()
go func() {
defer wg.Done()
mockMetadata, mockToken := c.deployManager.GetMockERC20(c.GetTestContext(), backend)
auth := backend.GetTxContext(c.GetTestContext(), mockMetadata.OwnerPtr()).TransactOpts
tx, err := mockToken.Mint(auth, c.wallet.Address(), big.NewInt(1000000000000000000))
c.Require().NoError(err)
backend.WaitForConfirmation(c.GetTestContext(), tx)
}()
}
server, err := relapi.NewRelayerAPI(c.GetTestContext(), c.cfg, c.handler, c.omniRPCClient, c.database, ts)
c.Require().NoError(err)
c.RelayerAPIServer = server
wg.Wait()
}
func (c *RelayerServerSuite) getMockTokenID(backend backends.SimulatedTestBackend) string {
erc20Metadata, _ := c.deployManager.GetMockERC20(c.GetTestContext(), backend)
return fmt.Sprintf("%d-%s", backend.GetChainID(), erc20Metadata.Address().Hex())
}
func (c *RelayerServerSuite) SetupSuite() {
c.TestSuite.SetupSuite()
c.deployManager = testutil.NewDeployManager(c.T())
// let's create 2 mock chains
chainIDs := []uint64{1, 42161}
c.testBackends = make(map[uint64]backends.SimulatedTestBackend)
testWallet, err := wallet.FromRandom()
c.Require().NoError(err)
c.testWallet = testWallet
g, _ := errgroup.WithContext(c.GetSuiteContext())
for _, chainID := range chainIDs {
// Setup Anvil backend for the suite to have RPC support
//nolint: copyloopvar
chainID := chainID // capture loop variable
g.Go(func() error {
backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID))
backend.FundAccount(c.GetSuiteContext(), c.testWallet.Address(), *big.NewInt(params.Ether))
// add the backend to the list of backends
c.testBackendMux.Lock()
defer c.testBackendMux.Unlock()
c.testBackends[chainID] = backend
c.omniRPCTestBackends = append(c.omniRPCTestBackends, backend)
return nil
})
}
// wait for all backends to be ready
if err := g.Wait(); err != nil {
c.T().Fatal(err)
}
c.fastBridgeAddressMap = xsync.NewIntegerMapOf[uint64, common.Address]()
g, _ = errgroup.WithContext(c.GetSuiteContext())
for _, backend := range c.testBackends {
backend := backend
// TODO: functionalize me
g.Go(func() error {
chainID, err := backend.ChainID(c.GetSuiteContext())
if err != nil {
return fmt.Errorf("could not get chain id: %w", err)
}
fastBridgeMetadata, _ := c.deployManager.GetFastBridge(c.GetSuiteContext(), backend)
// Save the contracts to the map
c.fastBridgeAddressMap.Store(chainID.Uint64(), fastBridgeMetadata.Address())
fastBridgeInstance, err := fastbridge.NewFastBridge(fastBridgeMetadata.Address(), backend)
c.Require().NoError(err)
relayerRole, err := fastBridgeInstance.RELAYERROLE(&bind.CallOpts{Context: c.GetSuiteContext()})
c.NoError(err)
auth := backend.GetTxContext(c.GetSuiteContext(), fastBridgeMetadata.OwnerPtr()).TransactOpts
tx, err := fastBridgeInstance.GrantRole(auth, relayerRole, c.testWallet.Address())
c.Require().NoError(err)
backend.WaitForConfirmation(c.GetSuiteContext(), tx)
return nil
})
g.Go(func() error {
mockERC20Metadata, mockERC20 := c.deployManager.GetMockERC20(c.GetSuiteContext(), backend)
if err != nil {
return fmt.Errorf("could not get mock ERC20: %w", err)
}
auth := backend.GetTxContext(c.GetSuiteContext(), mockERC20Metadata.OwnerPtr()).TransactOpts
mintTx, err := mockERC20.Mint(auth, c.testWallet.Address(), big.NewInt(1000000000000000000))
c.Require().NoError(err)
backend.WaitForConfirmation(c.GetSuiteContext(), mintTx)
return nil
})
}
dbType, err := dbcommon.DBTypeFromString("sqlite")
c.Require().NoError(err)
metricsHandler := metrics.NewNullHandler()
c.handler = metricsHandler
// TODO use temp file / in memory sqlite3 to not create in directory files
testDB, _ := connect.Connect(c.GetSuiteContext(), dbType, filet.TmpDir(c.T(), ""), metricsHandler)
c.database = testDB
// setup config
// wait for all backends to be ready
if err := g.Wait(); err != nil {
c.T().Fatal(err)
}
}
// TestConfigSuite runs the integration test suite.
func TestRelayerServerSuite(t *testing.T) {
suite.Run(t, NewRelayerServerSuite(t))
}
type RelayerClientSuite struct {
*testsuite.TestSuite
underlying *RelayerServerSuite
Client relapi.RelayerClient
}
// NewRelayerClientSuite creates a new relayer client suite.
func NewRelayerClientSuite(tb testing.TB) *RelayerClientSuite {
tb.Helper()
underlying := NewRelayerServerSuite(tb)
return &RelayerClientSuite{
TestSuite: underlying.TestSuite,
underlying: underlying,
}
}
func (c *RelayerClientSuite) SetupSuite() {
c.underlying.SetupSuite()
}
func (c *RelayerClientSuite) SetupTest() {
c.underlying.SetupTest()
c.underlying.startQuoterAPIServer()
c.Client = relapi.NewRelayerClient(c.underlying.handler, fmt.Sprintf("http://localhost:%d", c.underlying.port))
}
// TestConfigSuite runs the integration test suite.
func TestRelayerClientSuite(t *testing.T) {
suite.Run(t, NewRelayerClientSuite(t))
}