package api_test import ( "context" gosql "database/sql" "fmt" "math/big" "net/http" "testing" "time" serverConfig "github.com/synapsecns/sanguine/services/explorer/config/server" "github.com/synapsecns/sanguine/services/explorer/graphql/server/graph" "github.com/phayes/freeport" . "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/metrics" "github.com/synapsecns/sanguine/core/metrics/localmetrics" "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/core/testsuite" "github.com/synapsecns/sanguine/ethergo/backends" "github.com/synapsecns/sanguine/services/explorer/api" explorerclient "github.com/synapsecns/sanguine/services/explorer/consumer/client" "github.com/synapsecns/sanguine/services/explorer/db" "github.com/synapsecns/sanguine/services/explorer/db/sql" "github.com/synapsecns/sanguine/services/explorer/graphql/client" "github.com/synapsecns/sanguine/services/explorer/graphql/server" "github.com/synapsecns/sanguine/services/explorer/metadata" "github.com/synapsecns/sanguine/services/explorer/testutil" "github.com/synapsecns/sanguine/services/explorer/testutil/clickhouse" scribedb "github.com/synapsecns/sanguine/services/scribe/db" gqlServer "github.com/synapsecns/sanguine/services/scribe/graphql/server" scribeMetadata "github.com/synapsecns/sanguine/services/scribe/metadata" "go.uber.org/atomic" ) type MvBridgeEvent struct { InsertTime uint64 `gorm:"column:insert_time"` // FInsertTime is the time the event was inserted into the database. FInsertTime uint64 `gorm:"column:finsert_time"` // FContractAddress is the address of the contract that generated the event. FContractAddress string `gorm:"column:fcontract_address"` // FChainID is the chain id of the contract that generated the event. FChainID uint32 `gorm:"column:fchain_id"` // FEventType is the type of the event. FEventType uint8 `gorm:"column:fevent_type"` // FBlockNumber is the block number of the event. FBlockNumber uint64 `gorm:"column:fblock_number"` // FTxHash is the transaction hash of the event. FTxHash string `gorm:"column:ftx_hash"` // FToken is the address of the token. FToken string `gorm:"column:ftoken"` // FAmount is the amount of tokens. FAmount *big.Int `gorm:"column:famount;type:UInt256"` // FEventIndex is the index of the log. FEventIndex uint64 `gorm:"column:fevent_index"` // FDestinationKappa is the destination kappa. FDestinationKappa string `gorm:"column:fdestination_kappa"` // FSender is the address of the sender. FSender string `gorm:"column:fsender"` // FRecipient is the address to send the tokens to. FRecipient gosql.NullString `gorm:"column:frecipient"` // FRecipientBytes is the recipient address in bytes. FRecipientBytes gosql.NullString `gorm:"column:frecipient_bytes"` // FDestinationChainID is the chain id of the chain to send the tokens to. FDestinationChainID *big.Int `gorm:"column:fdestination_chain_id;type:UInt256"` // FFee is the fee. FFee *big.Int `gorm:"column:ffee;type:UInt256"` // FKappa is theFee keccak256 hash of the transaction. FKappa gosql.NullString `gorm:"column:fkappa"` // FTokenIndexFrom is the index of the from token in the pool. FTokenIndexFrom *big.Int `gorm:"column:ftoken_index_from;type:UInt256"` // FTokenIndexTo is the index of the to token in the pool. FTokenIndexTo *big.Int `gorm:"column:ftoken_index_to;type:UInt256"` // FMinDy is the minimum amount of tokens to receive. FMinDy *big.Int `gorm:"column:fmin_dy;type:UInt256"` // FDeadline is the deadline of the transaction. FDeadline *big.Int `gorm:"column:fdeadline;type:UInt256"` // FSwapSuccess is whether the swap was successful. FSwapSuccess *big.Int `gorm:"column:fswap_success;type:UInt256"` // FSwapTokenIndex is the index of the token in the pool. FSwapTokenIndex *big.Int `gorm:"column:fswap_token_index;type:UInt256"` // FSwapMinAmount is the minimum amount of tokens to receive. FSwapMinAmount *big.Int `gorm:"column:fswap_min_amount;type:UInt256"` // FSwapDeadline is the deadline of the swap transaction. FSwapDeadline *big.Int `gorm:"column:fswap_deadline;type:UInt256"` // FTokenID is the token's ID. FTokenID gosql.NullString `gorm:"column:ftoken_id"` // FAmountUSD is the amount in USD. FAmountUSD *float64 `gorm:"column:famount_usd;type:Float64"` // FFeeAmountUSD is the fee amount in USD. FFeeAmountUSD *float64 `gorm:"column:ffee_amount_usd;type:Float64"` // FTokenDecimal is the token's decimal. FTokenDecimal *uint8 `gorm:"column:ftoken_decimal"` // FTokenSymbol is the token's symbol from coin gecko. FTokenSymbol gosql.NullString `gorm:"column:ftoken_symbol"` // FTimeStamp is the timestamp of the block in which the event occurred. FTimeStamp *uint64 `gorm:"column:ftimestamp"` // TInsertTime is the time the event was inserted into the database. TInsertTime uint64 `gorm:"column:finsert_time"` // TContractAddress is the address of the contract that generated the event. TContractAddress string `gorm:"column:tcontract_address"` // TChainID is the chain id of the contract that generated the event. TChainID uint32 `gorm:"column:tchain_id"` // TEventType is the type of the event. TEventType uint8 `gorm:"column:tevent_type"` // TBlockNumber is the block number of the event. TBlockNumber uint64 `gorm:"column:tblock_number"` // TTxHash is the transaction hash of the event. TTxHash string `gorm:"column:ttx_hash"` // TToken is the address of the token. TToken string `gorm:"column:ttoken"` // TAmount is the amount of tokens. TAmount *big.Int `gorm:"column:tamount;type:UInt256"` // TEventIndex is the index of the log. TEventIndex uint64 `gorm:"column:tevent_index"` // TDestinationKappa is the destination kappa. TDestinationKappa string `gorm:"column:tdestination_kappa"` // TSender is the address of the sender. TSender string `gorm:"column:tsender"` // TRecipient is the address to send the tokens to. TRecipient gosql.NullString `gorm:"column:trecipient"` // TRecipientBytes is the recipient address in bytes. TRecipientBytes gosql.NullString `gorm:"column:trecipient_bytes"` // TDestinationChainID is the chain id of the chain to send the tokens to. TDestinationChainID *big.Int `gorm:"column:tdestination_chain_id;type:UInt256"` // TFee is the fee. TFee *big.Int `gorm:"column:tfee;type:UInt256"` // TKappa is theFee keccak256 hash of the transaction. TKappa gosql.NullString `gorm:"column:tkappa"` // TTokenIndexFrom is the index of the from token in the pool. TTokenIndexFrom *big.Int `gorm:"column:ttoken_index_from;type:UInt256"` // TTokenIndexTo is the index of the to token in the pool. TTokenIndexTo *big.Int `gorm:"column:ttoken_index_to;type:UInt256"` // TMinDy is the minimum amount of tokens to receive. TMinDy *big.Int `gorm:"column:tmin_dy;type:UInt256"` // TDeadline is the deadline of the transaction. TDeadline *big.Int `gorm:"column:tdeadline;type:UInt256"` // TSwapSuccess is whether the swap was successful. TSwapSuccess *big.Int `gorm:"column:tswap_success;type:UInt256"` // TSwapTokenIndex is the index of the token in the pool. TSwapTokenIndex *big.Int `gorm:"column:tswap_token_index;type:UInt256"` // TSwapMinAmount is the minimum amount of tokens to receive. TSwapMinAmount *big.Int `gorm:"column:tswap_min_amount;type:UInt256"` // TSwapDeadline is the deadline of the swap transaction. TSwapDeadline *big.Int `gorm:"column:tswap_deadline;type:UInt256"` // TTokenID is the token's ID. TTokenID gosql.NullString `gorm:"column:ttoken_id"` // TAmountUSD is the amount in USD. TAmountUSD *float64 `gorm:"column:tamount_usd;type:Float64"` // TFeeAmountUSD is the fee amount in USD. TFeeAmountUSD *float64 `gorm:"column:tfee_amount_usd;type:Float64"` // TTokenDecimal is the token's decimal. TTokenDecimal *uint8 `gorm:"column:ttoken_decimal"` // TTokenSymbol is the token's symbol from coin gecko. TTokenSymbol gosql.NullString `gorm:"column:ttoken_symbol"` // TTimeStamp is the timestamp of the block in which the event occurred. TTimeStamp *uint64 `gorm:"column:ttimestamp"` } // APISuite defines the basic test suite. type APISuite struct { *testsuite.TestSuite db db.ConsumerDB client *client.Client // grpcClient *rest.APIClient eventDB scribedb.EventDB gqlClient *explorerclient.Client logIndex atomic.Int64 cleanup func() testBackend backends.SimulatedTestBackend deployManager *testutil.DeployManager chainIDs []uint32 scribeMetrics metrics.Handler explorerMetrics metrics.Handler config serverConfig.Config } // NewTestSuite creates a new test suite and performs some basic checks afterward. // Every test suite in the synapse library should inherit from this suite and override where necessary. func NewTestSuite(tb testing.TB) *APISuite { tb.Helper() return &APISuite{ TestSuite: testsuite.NewTestSuite(tb), logIndex: atomic.Int64{}, } } func (g *APISuite) SetupSuite() { g.TestSuite.SetupSuite() // don't use metrics on ci for integration tests isCI := core.GetEnvBool("CI", false) useMetrics := !isCI metricsHandler := metrics.Null if useMetrics { localmetrics.SetupTestJaeger(g.GetSuiteContext(), g.T()) metricsHandler = metrics.Jaeger } var err error g.scribeMetrics, err = metrics.NewByType(g.GetSuiteContext(), scribeMetadata.BuildInfo(), metricsHandler) g.Require().Nil(err) // TODO: there may be an issue w/ syncer for local test nevs, investigate, but this probably comes from heavy load ending every span of every field synchronously g.explorerMetrics, err = metrics.NewByType(g.GetSuiteContext(), metadata.BuildInfo(), metrics.Null) g.Require().Nil(err) } func (g *APISuite) SetupTest() { g.TestSuite.SetupTest() initialFallback := graph.GetFallbackTime() graph.UnsafeSetFallbackTime(time.Second * 20) g.TestSuite.DeferAfterTest(func() { graph.UnsafeSetFallbackTime(initialFallback) }) g.db, g.eventDB, g.gqlClient, g.logIndex, g.cleanup, g.testBackend, g.deployManager = testutil.NewTestEnvDB(g.GetTestContext(), g.T(), g.scribeMetrics) cleanup, port, err := clickhouse.NewClickhouseStore("explorer") NotNil(g.T(), cleanup) NotNil(g.T(), port) Nil(g.T(), err) if port == nil || err != nil { g.TearDownTest() return } address := "clickhouse://clickhouse_test:clickhouse_test@localhost:" + fmt.Sprintf("%d", *port) + "/clickhouse_test" g.db, err = sql.OpenGormClickhouse(g.GetTestContext(), address, false, g.explorerMetrics) Nil(g.T(), err) err = g.db.UNSAFE_DB().WithContext(g.GetTestContext()).Set("gorm:table_options", "ENGINE=ReplacingMergeTree(finsert_time) ORDER BY (fevent_index, fblock_number, fevent_type, ftx_hash, fchain_id, fcontract_address)").AutoMigrate(&MvBridgeEvent{}) Nil(g.T(), err) g.chainIDs = []uint32{1, 10, 25, 56, 137} httpport := freeport.GetPort() config := serverConfig.Config{ HTTPPort: uint16(httpport), DBAddress: address, ScribeURL: "https://scribe.interoperability.institute/graphql", HydrateCache: false, RPCURL: "https://rpc.omnirpc.io/confirmations/1/rpc/", BridgeConfigAddress: "0x5217c83ca75559B1f8a8803824E5b7ac233A12a1", BridgeConfigChainID: 1, SwapTopicHash: "0xc6c1e0630dbe9130cc068028486c0d118ddcea348550819defd5cb8c257f8a38", Chains: map[uint32]serverConfig.ChainConfig{ 1: { ChainID: 1, GetLogsRange: 256, GetLogsBatchAmount: 1, BlockTime: 12, Swaps: []string{"0x1116898DdA4015eD8dDefb84b6e8Bc24528Af2d8"}, Contracts: serverConfig.ContractsConfig{ CCTP: "0xfB2Bfc368a7edfD51aa2cbEC513ad50edEa74E84", Bridge: "0x2796317b0fF8538F253012862c06787Adfb8cEb6", RFQ: "0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E", }, }, 56: { ChainID: 56, GetLogsRange: 1000, GetLogsBatchAmount: 1, BlockTime: 3, Swaps: []string{"0x28ec0B36F0819ecB5005cAB836F4ED5a2eCa4D13"}, Contracts: serverConfig.ContractsConfig{ Bridge: "0xd123f70AE324d34A9E76b67a27bf77593bA8749f", }, }, 42161: { ChainID: 42161, GetLogsRange: 1000, GetLogsBatchAmount: 1, BlockTime: 3, Swaps: []string{"0x9Dd329F5411466d9e0C488fF72519CA9fEf0cb40", "0xa067668661C84476aFcDc6fA5D758C4c01C34352"}, Contracts: serverConfig.ContractsConfig{ Bridge: "0x6F4e8eBa4D337f874Ab57478AcC2Cb5BACdc19c9", RFQ: "0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E", }, }, 10: { ChainID: 10, GetLogsRange: 1000, GetLogsBatchAmount: 1, BlockTime: 2, Swaps: []string{"0xF44938b0125A6662f9536281aD2CD6c499F22004", "0xE27BFf97CE92C3e1Ff7AA9f86781FDd6D48F5eE9"}, Contracts: serverConfig.ContractsConfig{ Bridge: "0xAf41a65F786339e7911F4acDAD6BD49426F2Dc6b", RFQ: "0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E", }, }, }, } g.config = config go func() { Nil(g.T(), api.Start(g.GetTestContext(), config, g.explorerMetrics)) }() baseURL := fmt.Sprintf("http://127.0.0.1:%d", httpport) g.client = client.NewClient(http.DefaultClient, fmt.Sprintf("%s%s", baseURL, gqlServer.GraphqlEndpoint)) err = retry.WithBackoff(g.GetTestContext(), func(ctx context.Context) error { request, err := http.NewRequestWithContext(g.GetTestContext(), http.MethodGet, fmt.Sprintf("%s%s", baseURL, server.GraphiqlEndpoint), nil) Nil(g.T(), err) res, err := g.client.Client.Client.Do(request) if err == nil { defer func() { _ = res.Body.Close() }() return nil } return fmt.Errorf("failed to connect to graphql server: %w", err) }, retry.WithMaxAttempts(1000)) g.Require().Nil(err) } func TestAPISuite(t *testing.T) { suite.Run(t, NewTestSuite(t)) }