synapsecns/sanguine

View on GitHub
services/rfq/e2e/setup_test.go

Summary

Maintainability
A
0 mins
Test Coverage
package e2e_test

import (
    "context"
    "fmt"
    "math/big"
    "net/http"
    "slices"
    "strconv"
    "sync"
    "time"

    "github.com/ethereum/go-ethereum/common"

    "github.com/Flaque/filet"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/params"
    "github.com/phayes/freeport"
    "github.com/synapsecns/sanguine/core"
    "github.com/synapsecns/sanguine/core/dbcommon"
    "github.com/synapsecns/sanguine/core/retry"
    "github.com/synapsecns/sanguine/core/testsuite"
    "github.com/synapsecns/sanguine/ethergo/backends"
    "github.com/synapsecns/sanguine/ethergo/backends/anvil"
    "github.com/synapsecns/sanguine/ethergo/backends/base"
    "github.com/synapsecns/sanguine/ethergo/contracts"
    signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config"
    "github.com/synapsecns/sanguine/ethergo/signer/wallet"
    cctpTest "github.com/synapsecns/sanguine/services/cctp-relayer/testutil"
    omnirpcClient "github.com/synapsecns/sanguine/services/omnirpc/client"
    "github.com/synapsecns/sanguine/services/omnirpc/testhelper"
    apiConfig "github.com/synapsecns/sanguine/services/rfq/api/config"
    "github.com/synapsecns/sanguine/services/rfq/api/db/sql"
    "github.com/synapsecns/sanguine/services/rfq/api/rest"
    "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20"
    "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig"
    guardConnect "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect"
    guardService "github.com/synapsecns/sanguine/services/rfq/guard/service"
    "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
    "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect"
    "github.com/synapsecns/sanguine/services/rfq/relayer/service"
    "github.com/synapsecns/sanguine/services/rfq/testutil"
    "github.com/synapsecns/sanguine/services/rfq/util"
)

func (i *IntegrationSuite) setupQuoterAPI() {
    dbPath := filet.TmpDir(i.T(), "")
    apiPort, err := freeport.GetFreePort()
    i.NoError(err)

    apiStore, err := sql.Connect(i.GetTestContext(), dbcommon.Sqlite, dbPath, i.metrics)
    i.NoError(err)

    // make the api without bridges
    apiCfg := apiConfig.Config{
        Database: apiConfig.DatabaseConfig{
            Type: dbcommon.Sqlite.String(),
            DSN:  dbPath,
        },
        OmniRPCURL: i.omniServer,
        Bridges: map[uint32]string{
            originBackendChainID: i.manager.Get(i.GetTestContext(), i.originBackend, testutil.FastBridgeType).Address().String(),
            destBackendChainID:   i.manager.Get(i.GetTestContext(), i.destBackend, testutil.FastBridgeType).Address().String(),
        },
        Port: strconv.Itoa(apiPort),
    }
    api, err := rest.NewAPI(i.GetTestContext(), apiCfg, i.metrics, i.omniClient, apiStore)
    i.NoError(err)

    i.apiServer = fmt.Sprintf("http://localhost:%d", apiPort)

    go func() {
        err = api.Run(i.GetTestContext())
        i.NoError(err)
    }()

    // make sure api server hast started
    testsuite.Eventually(i.GetTestContext(), i.T(), func() bool {
        var req *http.Request
        req, err = http.NewRequestWithContext(i.GetTestContext(), http.MethodGet, i.apiServer, nil)
        i.NoError(err)

        //nolint: bodyclose
        _, err = http.DefaultClient.Do(req)
        return err == nil
    })
}

// setupBackends sets up the ether backends and the omnirpc client/server
func (i *IntegrationSuite) setupBackends() {
    var wg sync.WaitGroup

    // Note: we're intentionally not gonna give these guys any tokens to allow the test to do it. What we will do is give them some eth and store the keys.
    var err error
    i.relayerWallet, err = wallet.FromRandom()
    i.NoError(err)

    i.guardWallet, err = wallet.FromRandom()
    i.NoError(err)

    i.userWallet, err = wallet.FromRandom()
    i.NoError(err)

    // Technically, we can use anvil for origin and geth for destination since only origin needs to use a block
    wg.Add(2)
    go func() {
        defer wg.Done()
        options := anvil.NewAnvilOptionBuilder()
        options.SetChainID(1)
        err = retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error {
            i.originBackend, err = anvil.NewAnvilBackend(i.GetTestContext(), i.T(), options)
            if err != nil {
                return fmt.Errorf("failed to create anvil backend: %w", err)
            }
            return nil
        }, retry.WithMaxTotalTime(5*time.Minute))
        i.Nil(err)
        i.setupBE(i.originBackend)
    }()
    go func() {
        defer wg.Done()
        options := anvil.NewAnvilOptionBuilder()
        options.SetChainID(destBackendChainID)
        err = retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error {
            i.destBackend, err = anvil.NewAnvilBackend(i.GetTestContext(), i.T(), options)
            if err != nil {
                return fmt.Errorf("failed to create anvil backend: %w", err)
            }
            return nil
        }, retry.WithMaxTotalTime(5*time.Minute))
        i.Nil(err)
        i.setupBE(i.destBackend)
    }()
    wg.Wait()

    i.omniServer = testhelper.NewOmnirpcServer(i.GetTestContext(), i.T(), i.originBackend, i.destBackend)
    i.omniClient = omnirpcClient.NewOmnirpcClient(i.omniServer, i.metrics, omnirpcClient.WithCaptureReqRes())

    i.setupCCTP()
}

// setupBe sets up one backend
func (i *IntegrationSuite) setupBE(backend backends.SimulatedTestBackend) {
    // prdeploys are contracts we want to deploy before running the test to speed it up. Obviously, these can be deployed when we need them as well,
    // but this way we can do something while we're waiting for the other backend to startup.
    // no need to wait for these to deploy since they can happen in background as soon as the backend is up.
    predeployTokens := []contracts.ContractType{testutil.DAIType, testutil.USDTType, testutil.WETH9Type}
    predeploys := append(predeployTokens, testutil.FastBridgeType)
    slices.Reverse(predeploys) // return fast bridge first

    ethAmount := *new(big.Int).Mul(big.NewInt(params.Ether), big.NewInt(10))

    // store the keys
    backend.Store(base.WalletToKey(i.T(), i.relayerWallet))
    backend.Store(base.WalletToKey(i.T(), i.guardWallet))
    backend.Store(base.WalletToKey(i.T(), i.userWallet))

    // fund each of the wallets
    backend.FundAccount(i.GetTestContext(), i.relayerWallet.Address(), ethAmount)
    backend.FundAccount(i.GetTestContext(), i.guardWallet.Address(), ethAmount)
    backend.FundAccount(i.GetTestContext(), i.userWallet.Address(), ethAmount)

    var wg sync.WaitGroup

    // TODO: in the case of relayer this not finishing before the test starts can lead to race conditions since
    // nonce may be shared between submitter and relayer. Think about how to deal w/ this.
    for _, user := range []wallet.Wallet{i.relayerWallet, i.guardWallet, i.userWallet} {
        wg.Add(1)
        go func(userWallet wallet.Wallet) {
            defer wg.Done()
            for _, token := range predeployTokens {
                i.Approve(backend, i.manager.Get(i.GetTestContext(), backend, token), userWallet)
            }
        }(user)
    }
    wg.Wait()
}

func (i *IntegrationSuite) setupCCTP() {
    // deploy the contract to all backends
    testBackends := core.ToSlice(i.originBackend, i.destBackend)

    // register remote deployments and tokens
    for _, b := range testBackends {
        backend := b
        err := retry.WithBackoff(i.GetTestContext(), func(_ context.Context) (err error) {
            cctpContract, cctpHandle := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backend)
            _, tokenMessengeHandle := i.cctpDeployManager.GetMockTokenMessengerType(i.GetTestContext(), backend)

            // on the above contract, set the remote for each backend
            for _, backendToSetFrom := range core.ToSlice(i.originBackend, i.destBackend) {
                // we don't need to set the backends own remote!
                if backendToSetFrom.GetChainID() == backend.GetChainID() {
                    continue
                }

                remoteCCTP, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backendToSetFrom)
                remoteMessenger, _ := i.cctpDeployManager.GetMockTokenMessengerType(i.GetTestContext(), backendToSetFrom)

                txOpts := backend.GetTxContext(i.GetTestContext(), cctpContract.OwnerPtr())
                // set the remote cctp contract on this cctp contract
                // TODO: verify chainID / domain are correct
                remoteDomain := cctpTest.ChainIDDomainMap[uint32(remoteCCTP.ChainID().Int64())]

                tx, err := cctpHandle.SetRemoteDomainConfig(txOpts.TransactOpts,
                    big.NewInt(remoteCCTP.ChainID().Int64()), remoteDomain, remoteCCTP.Address())
                if err != nil {
                    return fmt.Errorf("could not set remote domain config: %w", err)
                }
                backend.WaitForConfirmation(i.GetTestContext(), tx)

                // register the remote token messenger on the tokenMessenger contract
                _, err = tokenMessengeHandle.SetRemoteTokenMessenger(txOpts.TransactOpts, uint32(backendToSetFrom.GetChainID()), addressToBytes32(remoteMessenger.Address()))
                if err != nil {
                    return fmt.Errorf("could not set remote token messenger: %w", err)
                }
            }
            return nil
        }, retry.WithMaxTotalTime(30*time.Second))
        i.NoError(err)
    }
}

// addressToBytes32 converts an address to a bytes32.
func addressToBytes32(addr common.Address) [32]byte {
    var buf [32]byte
    copy(buf[:], addr[:])
    return buf
}

func (i *IntegrationSuite) waitForContractDeployment(ctx context.Context, backend backends.SimulatedTestBackend, address common.Address) error {
    // nolint: wrapcheck
    return retry.WithBackoff(ctx, func(_ context.Context) error {
        code, err := backend.CodeAt(ctx, address, nil)
        if err != nil {
            return fmt.Errorf("could not get code: %w", err)
        }
        if len(code) == 0 {
            return fmt.Errorf("contract not deployed at %s", address.Hex())
        }
        return nil
    }, retry.WithMaxTotalTime(30*time.Second))
}

func (i *IntegrationSuite) Approve(backend backends.SimulatedTestBackend, token contracts.DeployedContract, user wallet.Wallet) {
    err := i.waitForContractDeployment(i.GetTestContext(), backend, token.Address())
    i.Require().NoError(err, "Failed to wait for contract deployment")

    erc20, err := ierc20.NewIERC20(token.Address(), backend)
    i.Require().NoError(err, "Failed to get erc20")
    _, fastBridge := i.manager.GetFastBridge(i.GetTestContext(), backend)
    allowance, err := erc20.Allowance(&bind.CallOpts{Context: i.GetTestContext()}, user.Address(), fastBridge.Address())
    i.Require().NoError(err, "Failed to get allowance")

    if allowance.Cmp(big.NewInt(0)) == 0 {
        txOpts := backend.GetTxContext(i.GetTestContext(), user.AddressPtr())
        tx, err := erc20.Approve(txOpts.TransactOpts, fastBridge.Address(), core.CopyBigInt(abi.MaxUint256))
        i.Require().NoError(err, "Failed to approve")
        backend.WaitForConfirmation(i.GetTestContext(), tx)
    }
}

func (i *IntegrationSuite) getRelayerConfig() relconfig.Config {
    // construct the config
    relayerAPIPort, err := freeport.GetFreePort()
    i.NoError(err)
    dsn := filet.TmpDir(i.T(), "")
    cctpContractOrigin, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.originBackend)
    cctpContractDest, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.destBackend)
    return relconfig.Config{
        // generated ex-post facto
        Chains: map[int]relconfig.ChainConfig{
            originBackendChainID: {
                RFQAddress: i.manager.Get(i.GetTestContext(), i.originBackend, testutil.FastBridgeType).Address().String(),
                RebalanceConfigs: relconfig.RebalanceConfigs{
                    Synapse: &relconfig.SynapseCCTPRebalanceConfig{
                        SynapseCCTPAddress: cctpContractOrigin.Address().Hex(),
                    },
                },
                Confirmations: 0,
                Tokens: map[string]relconfig.TokenConfig{
                    "ETH": {
                        Address:  util.EthAddress.String(),
                        PriceUSD: 2000,
                        Decimals: 18,
                    },
                },
                NativeToken: "ETH",
            },
            destBackendChainID: {
                RFQAddress: i.manager.Get(i.GetTestContext(), i.destBackend, testutil.FastBridgeType).Address().String(),
                RebalanceConfigs: relconfig.RebalanceConfigs{
                    Synapse: &relconfig.SynapseCCTPRebalanceConfig{
                        SynapseCCTPAddress: cctpContractDest.Address().Hex(),
                    },
                },
                Confirmations: 0,
                Tokens: map[string]relconfig.TokenConfig{
                    "ETH": {
                        Address:  util.EthAddress.String(),
                        PriceUSD: 2000,
                        Decimals: 18,
                    },
                },
                NativeToken: "ETH",
            },
        },
        OmniRPCURL: i.omniServer,
        // TODO: need to stop hardcoding
        Database: relconfig.DatabaseConfig{
            Type: dbcommon.Sqlite.String(),
            DSN:  dsn,
        },
        // generated ex-post facto
        QuotableTokens: map[string][]string{},
        RFQAPIURL:      i.apiServer,
        Signer: signerConfig.SignerConfig{
            Type: signerConfig.FileType.String(),
            File: filet.TmpFile(i.T(), "", i.relayerWallet.PrivateKeyHex()).Name(),
        },
        RelayerAPIPort: strconv.Itoa(relayerAPIPort),
        BaseChainConfig: relconfig.ChainConfig{
            OriginGasEstimate: 500000,
            DestGasEstimate:   1000000,
        },
        FeePricer: relconfig.FeePricerConfig{
            GasPriceCacheTTLSeconds:   60,
            TokenPriceCacheTTLSeconds: 60,
        },
        RebalanceInterval: 0,
        VolumeLimit:       10_000,
    }
}

//nolint:gocognit,cyclop
func (i *IntegrationSuite) setupRelayer() {
    // add myself as a filler
    var wg sync.WaitGroup
    wg.Add(2)

    for _, backend := range core.ToSlice(i.originBackend, i.destBackend) {
        go func(backend backends.SimulatedTestBackend) {
            defer wg.Done()

            err := retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error {
                metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend)

                txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr())
                relayerRole, err := rfqContract.RELAYERROLE(&bind.CallOpts{Context: i.GetTestContext()})
                if err != nil {
                    return fmt.Errorf("could not get relayer role: %w", err)
                }

                tx, err := rfqContract.GrantRole(txContext.TransactOpts, relayerRole, i.relayerWallet.Address())
                if err != nil {
                    return fmt.Errorf("could not grant role: %w", err)
                }
                backend.WaitForConfirmation(i.GetTestContext(), tx)

                return nil
            }, retry.WithMaxTotalTime(15*time.Second))
            i.NoError(err)
        }(backend)
    }
    wg.Wait()

    cfg := i.getRelayerConfig()

    // in the first backend, we want to deploy a bunch of different tokens
    // TODO: functionalize me.
    for _, backend := range core.ToSlice(i.originBackend, i.destBackend) {
        tokenTypes := []contracts.ContractType{testutil.DAIType, testutil.USDTType, testutil.WETH9Type, cctpTest.MockMintBurnTokenType}

        for _, tokenType := range tokenTypes {
            useCCTP := tokenType == cctpTest.MockMintBurnTokenType
            var tokenAddress string
            if useCCTP {
                tokenAddress = i.cctpDeployManager.Get(i.GetTestContext(), backend, cctpTest.MockMintBurnTokenType).Address().String()
            } else {
                tokenAddress = i.manager.Get(i.GetTestContext(), backend, tokenType).Address().String()
            }
            quotableTokenID := fmt.Sprintf("%d-%s", backend.GetChainID(), tokenAddress)

            tokenCaller, err := ierc20.NewIerc20Ref(common.HexToAddress(tokenAddress), backend)
            i.NoError(err)

            decimals, err := tokenCaller.Decimals(&bind.CallOpts{Context: i.GetTestContext()})
            i.NoError(err)

            rebalanceMethod := ""
            if useCCTP {
                rebalanceMethod = "synapsecctp"
            }

            // first the simple part, add the token to the token map
            cfg.Chains[int(backend.GetChainID())].Tokens[tokenType.Name()] = relconfig.TokenConfig{
                Address:               tokenAddress,
                Decimals:              decimals,
                PriceUSD:              1, // TODO: this will break on non-stables
                RebalanceMethods:      []string{rebalanceMethod},
                MaintenanceBalancePct: 20,
                InitialBalancePct:     50,
            }

            compatibleTokens := []contracts.ContractType{tokenType}
            // DAI/USDC are fungible
            if tokenType == testutil.DAIType || tokenType == cctpTest.MockMintBurnTokenType {
                compatibleTokens = []contracts.ContractType{testutil.DAIType, cctpTest.MockMintBurnTokenType}
            }

            // now we need to add the token to the quotable tokens map
            for _, token := range compatibleTokens {
                otherBackend := i.getOtherBackend(backend)
                var otherToken string
                if token == cctpTest.MockMintBurnTokenType {
                    otherToken = i.cctpDeployManager.Get(i.GetTestContext(), otherBackend, cctpTest.MockMintBurnTokenType).Address().String()
                } else {
                    otherToken = i.manager.Get(i.GetTestContext(), otherBackend, token).Address().String()
                }

                cfg.QuotableTokens[quotableTokenID] = append(cfg.QuotableTokens[quotableTokenID], fmt.Sprintf("%d-%s", otherBackend.GetChainID(), otherToken))
            }

            // register the token with cctp contract
            cctpContract, cctpHandle := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backend)
            txOpts := backend.GetTxContext(i.GetTestContext(), cctpContract.OwnerPtr())
            tokenName := fmt.Sprintf("CCTP.%s", tokenType.Name())
            tx, err := cctpHandle.AddToken(txOpts.TransactOpts, tokenName, tokenCaller.Address(), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0))
            i.Require().NoError(err)
            backend.WaitForConfirmation(i.GetTestContext(), tx)
        }
    }

    // Add ETH as quotable token from origin to destination
    cfg.QuotableTokens[fmt.Sprintf("%d-%s", originBackendChainID, util.EthAddress)] = []string{
        fmt.Sprintf("%d-%s", destBackendChainID, util.EthAddress),
    }
    cfg.QuotableTokens[fmt.Sprintf("%d-%s", destBackendChainID, util.EthAddress)] = []string{
        fmt.Sprintf("%d-%s", originBackendChainID, util.EthAddress),
    }

    var err error
    i.relayer, err = service.NewRelayer(i.GetTestContext(), i.metrics, cfg)
    i.NoError(err)

    dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type)
    i.NoError(err)
    i.store, err = connect.Connect(i.GetTestContext(), dbType, cfg.Database.DSN, i.metrics)
    i.NoError(err)
}

func (i *IntegrationSuite) setupGuard() {
    // add myself as a guard
    var wg sync.WaitGroup
    wg.Add(2)

    for _, backend := range core.ToSlice(i.originBackend, i.destBackend) {
        go func(backend backends.SimulatedTestBackend) {
            defer wg.Done()

            metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend)

            txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr())
            guardRole, err := rfqContract.GUARDROLE(&bind.CallOpts{Context: i.GetTestContext()})
            i.NoError(err)

            tx, err := rfqContract.GrantRole(txContext.TransactOpts, guardRole, i.guardWallet.Address())
            i.NoError(err)

            backend.WaitForConfirmation(i.GetTestContext(), tx)
        }(backend)
    }
    wg.Wait()

    relayerCfg := i.getRelayerConfig()
    guardCfg := guardconfig.NewGuardConfigFromRelayer(relayerCfg)
    guardCfg.Signer = signerConfig.SignerConfig{
        Type: signerConfig.FileType.String(),
        File: filet.TmpFile(i.T(), "", i.guardWallet.PrivateKeyHex()).Name(),
    }

    var err error
    i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg, nil)
    i.NoError(err)

    dbType, err := dbcommon.DBTypeFromString(guardCfg.Database.Type)
    i.NoError(err)
    i.guardStore, err = guardConnect.Connect(i.GetTestContext(), dbType, guardCfg.Database.DSN, i.metrics)
    i.NoError(err)
}