From 3b80d3f16eeed69e67b55f83193a9dc2527176e3 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Fri, 28 Jun 2024 23:21:45 -0400 Subject: [PATCH 01/10] mimimal viable withdrawal api --- ethergo/submitter/submitter.go | 2 +- services/rfq/relayer/relapi/handler.go | 163 +++++++++++++++++++- services/rfq/relayer/relapi/handler_test.go | 34 ++++ services/rfq/relayer/relapi/server.go | 30 ++-- services/rfq/relayer/relconfig/config.go | 24 +++ 5 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 services/rfq/relayer/relapi/handler_test.go diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index df1c107ddd..8710cb7827 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -39,7 +39,7 @@ import ( var logger = log.Logger("ethergo-submitter") -const meterName = "github.com/synapsecns/sanguine/services/rfq/api/rest" +const meterName = "github.com/synapsecns/sanguine/ethergo/submitter" // TransactionSubmitter is the interface for submitting transactions to the chain. type TransactionSubmitter interface { diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 3b80387b46..7c5c5850a7 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -1,7 +1,14 @@ package relapi import ( + "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/ethergo/submitter" + "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" @@ -13,15 +20,19 @@ import ( // Handler is the REST API handler. type Handler struct { - db reldb.Service - chains map[uint32]*chain.Chain + db reldb.Service + chains map[uint32]*chain.Chain + cfg relconfig.Config + submitter submitter.TransactionSubmitter } // NewHandler creates a new REST API handler. -func NewHandler(db reldb.Service, chains map[uint32]*chain.Chain) *Handler { +func NewHandler(db reldb.Service, chains map[uint32]*chain.Chain, cfg relconfig.Config, txSubmitter submitter.TransactionSubmitter) *Handler { return &Handler{ - db: db, // Store the database connection in the handler - chains: chains, + db: db, // Store the database connection in the handler + chains: chains, + cfg: cfg, + submitter: txSubmitter, } } @@ -107,14 +118,14 @@ func (h *Handler) GetTxRetry(c *gin.Context) { } chainID := quoteRequest.Transaction.DestChainId - chain, ok := h.chains[chainID] + chainHandler, ok := h.chains[chainID] if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No contract found for chain: %d", chainID)}) return } // `quoteRequest == nil` case should be handled by the db query above - nonce, gasAmount, err := chain.SubmitRelay(c, *quoteRequest) + nonce, gasAmount, err := chainHandler.SubmitRelay(c, *quoteRequest) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit relay: %s", err.Error())}) return @@ -128,3 +139,141 @@ func (h *Handler) GetTxRetry(c *gin.Context) { } c.JSON(http.StatusOK, resp) } + +// Withdraw withdraws tokens from the relayer. +func (h *Handler) Withdraw(c *gin.Context) { + var req WithdrawRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // validate the token address + if !tokenIDExists(h.cfg, req.TokenAddress, int(req.ChainID)) { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid token address %s for chain %d", req.TokenAddress.Hex(), req.ChainID)}) + return + } + + // validate the withdrawal address + if !toAddressIsWhitelisted(h.cfg, req.To) { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("withdrawal address %s is not whitelisted", req.To.Hex())}) + return + } + + var nonce uint64 + var err error + + value, ok := new(big.Int).SetString(req.Amount, 10) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid amount %s", req.Amount)}) + return + } + + if chain.IsGasToken(req.TokenAddress) { + nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + tx = types.NewTx(&types.LegacyTx{ + Nonce: transactor.Nonce.Uint64(), + To: &req.To, + Value: value, + }) + + return tx, nil + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())}) + return + } + } else { + erc20Contract, err := ierc20.NewIERC20(req.TokenAddress, h.chains[req.ChainID].Client) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not create erc20 contract: %s", err.Error())}) + return + } + + nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + tx, err = erc20Contract.Transfer(transactor, req.To, value) + return tx, fmt.Errorf("could not create transfer transaction: %w", err) + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())}) + return + } + } + + c.JSON(http.StatusOK, gin.H{"nonce": nonce}) +} + +// tokenIDExists checks if a token ID exists in the config. +// note: this method assumes that SanitizeTokenID is a method of relconfig.Config +func tokenIDExists(cfg relconfig.Config, tokenAddress common.Address, chainID int) bool { + for quotableToken := range cfg.QuotableTokens { + prospectiveChainID, prospectiveAddress, err := relconfig.DecodeTokenID(quotableToken) + if err != nil { + continue + } + + if prospectiveChainID == chainID && prospectiveAddress == tokenAddress { + return true + } + } + + return false +} + +func toAddressIsWhitelisted(cfg relconfig.Config, to common.Address) bool { + for _, addr := range cfg.WithdrawalWhitelist { + if common.HexToAddress(addr) == to { + return true + } + } + return false +} + +// WithdrawRequest is the request to withdraw tokens from the relayer. +type WithdrawRequest struct { + // ChainID is the chain ID of the chain to withdraw from. + ChainID uint32 `json:"chain_id"` + // Amount is the amount to withdraw, in wei. + Amount string `json:"amount"` + // TokenAddress is the address of the token to withdraw. + TokenAddress common.Address `json:"token_address"` + // To is the address to withdraw to. + To common.Address `json:"to"` +} + +// MarshalJSON handles JSON marshaling for WithdrawRequest. +func (wr *WithdrawRequest) MarshalJSON() ([]byte, error) { + type Alias WithdrawRequest + // nolint: wrapcheck + return json.Marshal(&struct { + TokenAddress string `json:"token_address"` + To string `json:"to"` + *Alias + }{ + TokenAddress: wr.TokenAddress.Hex(), + To: wr.To.Hex(), + Alias: (*Alias)(wr), + }) +} + +// UnmarshalJSON has JSON unmarshalling for WithdrawRequest. +func (wr *WithdrawRequest) UnmarshalJSON(data []byte) error { + type Alias WithdrawRequest + aux := &struct { + TokenAddress string `json:"token_address"` + To string `json:"to"` + *Alias + }{ + Alias: (*Alias)(wr), + } + + if err := json.Unmarshal(data, aux); err != nil { + //nolint: wrapcheck + return err + } + + wr.TokenAddress = common.HexToAddress(aux.TokenAddress) + wr.To = common.HexToAddress(aux.To) + + return nil +} diff --git a/services/rfq/relayer/relapi/handler_test.go b/services/rfq/relayer/relapi/handler_test.go new file mode 100644 index 0000000000..d3c52517c4 --- /dev/null +++ b/services/rfq/relayer/relapi/handler_test.go @@ -0,0 +1,34 @@ +package relapi_test + +import ( + "encoding/json" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" + "testing" +) + +func TestWithdrawRequestJSON(t *testing.T) { + original := relapi.WithdrawRequest{ + ChainID: 1, + Amount: "100", + TokenAddress: common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), + To: common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"), + } + + // Marshal to JSON + data, err := json.Marshal(original) + assert.NoError(t, err) + + // Unmarshal back to struct + var unmarshalled relapi.WithdrawRequest + err = json.Unmarshal(data, &unmarshalled) + assert.NoError(t, err) + + // Check if the original and unmarshalled structs are the same + assert.Equal(t, original, unmarshalled) + + // Check the JSON string explicitly + expectedJSON := `{"chain_id":1,"amount":"100","token_address":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","to":"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}` + assert.JSONEq(t, expectedJSON, string(data)) +} diff --git a/services/rfq/relayer/relapi/server.go b/services/rfq/relayer/relapi/server.go index 46d3354311..709a6c3b12 100644 --- a/services/rfq/relayer/relapi/server.go +++ b/services/rfq/relayer/relapi/server.go @@ -25,11 +25,12 @@ import ( // RelayerAPIServer is a struct that holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. // It is used to initialize and run the API server. type RelayerAPIServer struct { - cfg relconfig.Config - db reldb.Service - engine *gin.Engine - handler metrics.Handler - chains map[uint32]*chain.Chain + cfg relconfig.Config + db reldb.Service + engine *gin.Engine + handler metrics.Handler + chains map[uint32]*chain.Chain + submitter submitter.TransactionSubmitter } // NewRelayerAPI holds the configuration, database connection, gin engine, RPC client, metrics handler, and fast bridge contracts. @@ -86,10 +87,11 @@ func NewRelayerAPI( } return &RelayerAPIServer{ - cfg: cfg, - db: store, - handler: handler, - chains: chains, + cfg: cfg, + db: store, + handler: handler, + chains: chains, + submitter: submitter, }, nil } @@ -98,15 +100,17 @@ const ( getQuoteStatusByTxHashRoute = "/status" getQuoteStatusByTxIDRoute = "/status/by_tx_id" getRetryRoute = "/retry" + postWithdrawRoute = "/withdraw" ) var logger = log.Logger("relayer-api") // Run runs the rest api server. func (r *RelayerAPIServer) Run(ctx context.Context) error { - // TODO: Use Gin Helper engine := ginhelper.New(logger) - h := NewHandler(r.db, r.chains) + // default tracing middleware + engine.Use(r.handler.Gin()...) + h := NewHandler(r.db, r.chains, r.cfg, r.submitter) // Assign GET routes engine.GET(getHealthRoute, h.GetHealth) @@ -115,6 +119,10 @@ func (r *RelayerAPIServer) Run(ctx context.Context) error { engine.GET(getRetryRoute, h.GetTxRetry) engine.GET(metrics.MetricsPathDefault, gin.WrapH(r.handler.Handler())) + if r.cfg.EnableAPIWithdrawals { + engine.POST(postWithdrawRoute, h.Withdraw) + } + r.engine = engine connection := baseServer.Server{} diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 1c4222fa10..00055b11e1 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -51,6 +51,10 @@ type Config struct { QuoteSubmissionTimeout time.Duration `yaml:"quote_submission_timeout"` // CCTPRelayerConfig is the embedded cctp relayer config (optional). CCTPRelayerConfig *cctpConfig.Config `yaml:"cctp_relayer_config"` + // EnableAPIWithdrawals enables withdrawals via the API. + EnableAPIWithdrawals bool `yaml:"enable_api_withdrawals"` + // WithdrawalWhitelist is a list of addresses that are allowed to withdraw. + WithdrawalWhitelist []string `yaml:"withdrawal_whitelist"` } // ChainConfig represents the configuration for a chain. @@ -156,6 +160,26 @@ func SanitizeTokenID(id string) (sanitized string, err error) { return sanitized, nil } +// DecodeTokenID decodes a token ID into a chain ID and address. +func DecodeTokenID(id string) (chainID int, addr common.Address, err error) { + // defensive coding, first check if the token ID is valid + _, err = SanitizeTokenID(id) + if err != nil { + return chainID, addr, err + } + + split := strings.Split(id, tokenIDDelimiter) + if len(split) != 2 { + return chainID, addr, fmt.Errorf("invalid token ID: %s", id) + } + chainID, err = strconv.Atoi(split[0]) + if err != nil { + return chainID, addr, fmt.Errorf("invalid chain ID: %s", split[0]) + } + addr = common.HexToAddress(split[1]) + return chainID, addr, nil +} + // LoadConfig loads the config from the given path. func LoadConfig(path string) (config Config, err error) { input, err := os.ReadFile(filepath.Clean(path)) From 92583691e975a43d1e5f7bf0447e6f13a07d7112 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 29 Jun 2024 00:44:51 -0400 Subject: [PATCH 02/10] eth withdraw test --- ethergo/submitter/submitter.go | 6 + services/rfq/relayer/relapi/client.go | 22 ++++ services/rfq/relayer/relapi/handler.go | 2 +- services/rfq/relayer/relapi/server_test.go | 34 ++++++ services/rfq/relayer/relapi/suite_test.go | 135 ++++++++++++++++----- 5 files changed, 166 insertions(+), 33 deletions(-) diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index 8710cb7827..93cb600a94 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -51,6 +51,8 @@ type TransactionSubmitter interface { SubmitTransaction(ctx context.Context, chainID *big.Int, call ContractCallType) (nonce uint64, err error) // GetSubmissionStatus returns the status of a transaction and any metadata associated with it if it is complete. GetSubmissionStatus(ctx context.Context, chainID *big.Int, nonce uint64) (status SubmissionStatus, err error) + // Address returns the address of the signer. + Address() common.Address } // txSubmitterImpl is the implementation of the transaction submitter. @@ -683,4 +685,8 @@ func (t *txSubmitterImpl) getGasEstimate(ctx context.Context, chainClient client return gasEstimate, nil } +func (t *txSubmitterImpl) Address() common.Address { + return t.signer.Address() +} + var _ TransactionSubmitter = &txSubmitterImpl{} diff --git a/services/rfq/relayer/relapi/client.go b/services/rfq/relayer/relapi/client.go index 59add9ccd0..cd643cec4c 100644 --- a/services/rfq/relayer/relapi/client.go +++ b/services/rfq/relayer/relapi/client.go @@ -17,6 +17,7 @@ type RelayerClient interface { GetQuoteRequestStatusByTxHash(ctx context.Context, hash string) (*GetQuoteRequestStatusResponse, error) GetQuoteRequestStatusByTxID(ctx context.Context, hash string) (*GetQuoteRequestStatusResponse, error) RetryTransaction(ctx context.Context, txhash string) (*GetTxRetryResponse, error) + Withdraw(ctx context.Context, req *WithdrawRequest) (*WithdrawResponse, error) } type relayerClient struct { @@ -100,3 +101,24 @@ func (r *relayerClient) RetryTransaction(ctx context.Context, txhash string) (*G return &res, nil } + +// WithdrawResponse is the response for the withdraw request. +type WithdrawResponse struct { + Nonce uint64 `json:"nonce"` +} + +func (r *relayerClient) Withdraw(ctx context.Context, req *WithdrawRequest) (*WithdrawResponse, error) { + var res WithdrawResponse + resp, err := r.client.R().SetContext(ctx). + SetResult(&res). + SetBody(req). + Post(postWithdrawRoute) + if err != nil { + return nil, fmt.Errorf("failed to withdraw transaction: %w", err) + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + + return &res, nil +} diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 7c5c5850a7..76643727c8 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -177,7 +177,7 @@ func (h *Handler) Withdraw(c *gin.Context) { Value: value, }) - return tx, nil + return transactor.Signer(h.submitter.Address(), tx) }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())}) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 24ff6f5ccf..ca8bdb5fe7 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "math/big" "net/http" "time" @@ -202,3 +203,36 @@ func (c *RelayerServerSuite) getTestQuoteRequest(status reldb.QuoteRequestStatus DestTxHash: common.HexToHash("0x0000001"), } } + +func (c *RelayerClientSuite) TestEthWithdraw() { + backend := c.underlying.testBackends[uint64(c.underlying.originChainID)] + + startBalance, err := backend.BalanceAt(c.GetTestContext(), testWithdrawalAddress, nil) + c.Require().NoError(err) + + withdrawalAmount := big.NewInt(50) + + _, err = c.Client.Withdraw(c.GetTestContext(), &relapi.WithdrawRequest{ + ChainID: uint32(backend.GetChainID()), + To: testWithdrawalAddress, + Amount: withdrawalAmount.String(), + TokenAddress: chain.EthAddress, + }) + c.Require().NoError(err) + + // Wait for the transaction to be mined + err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + balance, err := backend.BalanceAt(ctx, testWithdrawalAddress, nil) + if err != nil { + return err + } + + expectedBalance := new(big.Int).Add(startBalance, withdrawalAmount) + + if balance.Cmp(expectedBalance) != 0 { + return fmt.Errorf("balance not updated") + } + + return nil + }) +} diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index ff16728fb8..237d4f6a36 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -2,14 +2,17 @@ package relapi_test import ( "fmt" + "github.com/ethereum/go-ethereum/params" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" + "github.com/synapsecns/sanguine/services/rfq/testutil" "math/big" "strconv" + "sync" "testing" "github.com/Flaque/filet" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" "github.com/phayes/freeport" "github.com/puzpuzpuz/xsync/v2" "github.com/stretchr/testify/suite" @@ -35,8 +38,10 @@ import ( // 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 @@ -50,6 +55,8 @@ type RelayerServerSuite struct { 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() @@ -90,6 +97,25 @@ func (c *RelayerServerSuite) SetupTest() { 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, chain.EthAddress): { + // not used for this test + }, + fmt.Sprintf("%d-%s", c.destChainID, chain.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 @@ -99,31 +125,72 @@ func (c *RelayerServerSuite) SetupTest() { 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 { + backend := backend + 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 - // anvilOpts := anvil.NewAnvilOptionBuilder() - // anvilOpts.SetChainID(chainID) - // anvilOpts.SetBlockTime(1 * time.Second) - // backend := anvil.NewAnvilBackend(c.GetSuiteContext(), c.T(), anvilOpts) - backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID)) - - // add the backend to the list of backends - c.testBackends[chainID] = backend - c.omniRPCTestBackends = append(c.omniRPCTestBackends, backend) + 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 @@ -131,13 +198,6 @@ func (c *RelayerServerSuite) SetupSuite() { c.T().Fatal(err) } - testWallet, err := wallet.FromRandom() - c.Require().NoError(err) - c.testWallet = testWallet - for _, backend := range c.testBackends { - backend.FundAccount(c.GetSuiteContext(), c.testWallet.Address(), *big.NewInt(params.Ether)) - } - c.fastBridgeAddressMap = xsync.NewIntegerMapOf[uint64, common.Address]() g, _ = errgroup.WithContext(c.GetSuiteContext()) @@ -149,35 +209,41 @@ func (c *RelayerServerSuite) SetupSuite() { if err != nil { return fmt.Errorf("could not get chain id: %w", err) } - // Create an auth to interact with the blockchain - auth, err := bind.NewKeyedTransactorWithChainID(c.testWallet.PrivateKey(), chainID) - c.Require().NoError(err) - // Deploy the FastBridge contract - fastBridgeAddress, tx, _, err := fastbridge.DeployFastBridge(auth, backend, c.testWallet.Address()) - c.Require().NoError(err) - backend.WaitForConfirmation(c.GetSuiteContext(), tx) + fastBridgeMetadata, _ := c.deployManager.GetFastBridge(c.GetSuiteContext(), backend) // Save the contracts to the map - c.fastBridgeAddressMap.Store(chainID.Uint64(), fastBridgeAddress) + c.fastBridgeAddressMap.Store(chainID.Uint64(), fastBridgeMetadata.Address()) - fastBridgeInstance, err := fastbridge.NewFastBridge(fastBridgeAddress, backend) + fastBridgeInstance, err := fastbridge.NewFastBridge(fastBridgeMetadata.Address(), backend) c.Require().NoError(err) - relayerRole, err := fastBridgeInstance.RELAYERROLE(&bind.CallOpts{Context: c.GetTestContext()}) + relayerRole, err := fastBridgeInstance.RELAYERROLE(&bind.CallOpts{Context: c.GetSuiteContext()}) c.NoError(err) - tx, err = fastBridgeInstance.GrantRole(auth, relayerRole, c.testWallet.Address()) + 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 }) - } - // wait for all backends to be ready - if err := g.Wait(); err != nil { - c.T().Fatal(err) + 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") @@ -188,6 +254,11 @@ func (c *RelayerServerSuite) SetupSuite() { 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. From a17db241029379d9b68b601d3230083dc469443f Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 29 Jun 2024 00:51:30 -0400 Subject: [PATCH 03/10] rfq api --- services/rfq/relayer/relapi/handler.go | 7 +++-- services/rfq/relayer/relapi/server_test.go | 36 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 76643727c8..9ceae04946 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -172,7 +172,8 @@ func (h *Handler) Withdraw(c *gin.Context) { if chain.IsGasToken(req.TokenAddress) { nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { tx = types.NewTx(&types.LegacyTx{ - Nonce: transactor.Nonce.Uint64(), + // covers l2s, etc + Gas: 500_000, To: &req.To, Value: value, }) @@ -191,8 +192,8 @@ func (h *Handler) Withdraw(c *gin.Context) { } nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - tx, err = erc20Contract.Transfer(transactor, req.To, value) - return tx, fmt.Errorf("could not create transfer transaction: %w", err) + // nolint: wrapcheck + return erc20Contract.Transfer(transactor, req.To, value) }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())}) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index ca8bdb5fe7..18e6dcc9de 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "math/big" "net/http" @@ -236,3 +237,38 @@ func (c *RelayerClientSuite) TestEthWithdraw() { return nil }) } + +func (c *RelayerClientSuite) TestERC20Withdraw() { + backend := c.underlying.testBackends[uint64(c.underlying.originChainID)] + + _, erc20 := c.underlying.deployManager.GetMockERC20(c.GetTestContext(), backend) + + startBalance, err := erc20.BalanceOf(&bind.CallOpts{Context: c.GetTestContext()}, testWithdrawalAddress) + c.Require().NoError(err) + + withdrawalAmount := big.NewInt(50) + + _, err = c.Client.Withdraw(c.GetTestContext(), &relapi.WithdrawRequest{ + ChainID: uint32(backend.GetChainID()), + To: testWithdrawalAddress, + Amount: withdrawalAmount.String(), + TokenAddress: erc20.Address(), + }) + c.Require().NoError(err) + + // Wait for the transaction to be mined + err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + balance, err := erc20.BalanceOf(&bind.CallOpts{Context: ctx}, testWithdrawalAddress) + if err != nil { + return err + } + + expectedBalance := new(big.Int).Add(startBalance, withdrawalAmount) + + if balance.Cmp(expectedBalance) != 0 { + return fmt.Errorf("balance not updated") + } + + return nil + }) +} From 484a9a395ee9cf8c60993e287e017a8b31324f63 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 29 Jun 2024 00:59:31 -0400 Subject: [PATCH 04/10] withdrawal api --- services/rfq/relayer/relapi/handler.go | 13 +++++++------ services/rfq/relayer/relapi/server_test.go | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index 9ceae04946..c9a76b1a53 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -3,6 +3,7 @@ package relapi import ( "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" "github.com/synapsecns/sanguine/ethergo/submitter" @@ -171,12 +172,12 @@ func (h *Handler) Withdraw(c *gin.Context) { if chain.IsGasToken(req.TokenAddress) { nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - tx = types.NewTx(&types.LegacyTx{ - // covers l2s, etc - Gas: 500_000, - To: &req.To, - Value: value, - }) + bc := bind.NewBoundContract(req.To, abi.ABI{}, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client) + if transactor.GasPrice != nil { + transactor.Value = value + // nolint: wrapcheck + return bc.Transfer(transactor) + } return transactor.Signer(h.submitter.Address(), tx) }) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 18e6dcc9de..256a8f42ca 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -236,6 +236,7 @@ func (c *RelayerClientSuite) TestEthWithdraw() { return nil }) + c.Require().NoError(err) } func (c *RelayerClientSuite) TestERC20Withdraw() { @@ -271,4 +272,5 @@ func (c *RelayerClientSuite) TestERC20Withdraw() { return nil }) + c.Require().NoError(err) } From e83dd9d4e67e1394788421cfa86ec708b58ea8e5 Mon Sep 17 00:00:00 2001 From: vro <168573323+golangisfun123@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:32:35 -0500 Subject: [PATCH 05/10] RFQ Minimal Withdraw CLI(#2826) Co-authored-by: Trajan0x --- contrib/opbot/config/config.go | 5 +- go.work.sum | 2 + services/rfq/relayer/cmd/cmd.go | 2 +- services/rfq/relayer/cmd/commands.go | 78 +++++++++++++++++++++ services/rfq/relayer/relapi/handler.go | 15 ++-- services/rfq/relayer/relapi/server_test.go | 79 ++++++++++++++++++++-- services/rfq/relayer/relapi/suite_test.go | 9 +-- 7 files changed, 175 insertions(+), 15 deletions(-) diff --git a/contrib/opbot/config/config.go b/contrib/opbot/config/config.go index 7a6e1d9800..5701c876ef 100644 --- a/contrib/opbot/config/config.go +++ b/contrib/opbot/config/config.go @@ -15,6 +15,7 @@ type Config struct { // inject only with init container! SignozPassword string `yaml:"signoz_password"` // SignozBaseURL is the base url of the signoz instance. - SignozBaseURL string `yaml:"signoz_base_url"` - RelayerURLS []string `yaml:"rfq_relayer_urls"` + SignozBaseURL string `yaml:"signoz_base_url"` + // RelayerURLS is the list of RFQ relayer URLs. + RelayerURLS []string `yaml:"rfq_relayer_urls"` } diff --git a/go.work.sum b/go.work.sum index a9ab7743f0..a609c5661d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1638,6 +1638,7 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0= github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= @@ -1993,6 +1994,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= github.com/crate-crypto/go-ipa v0.0.0-20220523130400-f11357ae11c7 h1:6IrxszG5G+O7zhtkWxq6+unVvnrm1fqV2Pe+T95DUzw= diff --git a/services/rfq/relayer/cmd/cmd.go b/services/rfq/relayer/cmd/cmd.go index 1a0ef3b33a..b92c930ad8 100644 --- a/services/rfq/relayer/cmd/cmd.go +++ b/services/rfq/relayer/cmd/cmd.go @@ -23,7 +23,7 @@ func Start(args []string, buildInfo config.BuildInfo) { } // commands - app.Commands = cli.Commands{runCommand} + app.Commands = cli.Commands{runCommand, withdrawCommand} shellCommand := commandline.GenerateShellCommand(app.Commands) app.Commands = append(app.Commands, shellCommand) app.Action = shellCommand.Action diff --git a/services/rfq/relayer/cmd/commands.go b/services/rfq/relayer/cmd/commands.go index 3f06fba3d0..728eff0336 100644 --- a/services/rfq/relayer/cmd/commands.go +++ b/services/rfq/relayer/cmd/commands.go @@ -4,9 +4,11 @@ package cmd import ( "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/commandline" "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/services/rfq/relayer/relapi" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/service" "github.com/urfave/cli/v2" @@ -44,3 +46,79 @@ var runCommand = &cli.Command{ return nil }, } + +var relayerURLFlag = &cli.StringFlag{ + Name: "relayer-url", + Usage: "relayer url", +} + +var chainIDFlag = &cli.StringFlag{ + Name: "chain-id", + Usage: "chain id", +} + +var amountFlag = &cli.StringFlag{ + Name: "amount", + Usage: "amount", +} + +var tokenAddressFlag = &cli.StringFlag{ + Name: "token-address", + Usage: "token address", +} + +var toFlag = &cli.StringFlag{ + Name: "to", + Usage: "to", +} + +// runCommand runs the rfq relayer. +var withdrawCommand = &cli.Command{ + Name: "widthdraw", + Description: "run the withdrawal tool", + Flags: []cli.Flag{relayerURLFlag, chainIDFlag, amountFlag, tokenAddressFlag, toFlag, &commandline.LogLevel}, + Action: func(c *cli.Context) (err error) { + if c.String(relayerURLFlag.Name) == "" { + return fmt.Errorf("relayer URL is required") + } + + withdrawer := relapi.NewRelayerClient(metrics.Get(), c.String(relayerURLFlag.Name)) + if err != nil { + return fmt.Errorf("could not create relayer: %w", err) + } + + chainID := c.Uint(chainIDFlag.Name) + if chainID == 0 { + return fmt.Errorf("valid chain ID is required") + } + + amount := c.String(amountFlag.Name) + if amount == "" { + return fmt.Errorf("amount is required") + } + + tokenAddress := c.String(tokenAddressFlag.Name) + if !common.IsHexAddress(tokenAddress) { + return fmt.Errorf("valid token address is required") + } + + to := c.String(toFlag.Name) + if !common.IsHexAddress(to) { + return fmt.Errorf("valid recipient address is required") + } + + withdrawRequest := relapi.WithdrawRequest{ + ChainID: uint32(chainID), + Amount: amount, + TokenAddress: common.HexToAddress(tokenAddress), + To: common.HexToAddress(to), + } + + _, err = withdrawer.Withdraw(c.Context, &withdrawRequest) + if err != nil { + return fmt.Errorf("could not start relayer: %w", err) + } + + return nil + }, +} diff --git a/services/rfq/relayer/relapi/handler.go b/services/rfq/relayer/relapi/handler.go index c9a76b1a53..9b5d741cbd 100644 --- a/services/rfq/relayer/relapi/handler.go +++ b/services/rfq/relayer/relapi/handler.go @@ -3,14 +3,15 @@ package relapi import ( "encoding/json" "fmt" + "math/big" + "net/http" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" "github.com/synapsecns/sanguine/ethergo/submitter" "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" - "math/big" - "net/http" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -142,6 +143,8 @@ func (h *Handler) GetTxRetry(c *gin.Context) { } // Withdraw withdraws tokens from the relayer. +// +//nolint:cyclop func (h *Handler) Withdraw(c *gin.Context) { var req WithdrawRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -170,6 +173,7 @@ func (h *Handler) Withdraw(c *gin.Context) { return } + //nolint: nestif if chain.IsGasToken(req.TokenAddress) { nonce, err = h.submitter.SubmitTransaction(c, big.NewInt(int64(req.ChainID)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { bc := bind.NewBoundContract(req.To, abi.ABI{}, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client, h.chains[req.ChainID].Client) @@ -178,8 +182,11 @@ func (h *Handler) Withdraw(c *gin.Context) { // nolint: wrapcheck return bc.Transfer(transactor) } - - return transactor.Signer(h.submitter.Address(), tx) + signer, err := transactor.Signer(h.submitter.Address(), tx) + if err != nil { + return nil, fmt.Errorf("could not get signer: %w", err) + } + return signer, nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not submit transaction: %s", err.Error())}) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 256a8f42ca..2015cc91e7 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "math/big" "net/http" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" @@ -225,7 +226,7 @@ func (c *RelayerClientSuite) TestEthWithdraw() { err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { balance, err := backend.BalanceAt(ctx, testWithdrawalAddress, nil) if err != nil { - return err + return fmt.Errorf("could not fetch balance %w", err) } expectedBalance := new(big.Int).Add(startBalance, withdrawalAmount) @@ -261,7 +262,75 @@ func (c *RelayerClientSuite) TestERC20Withdraw() { err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { balance, err := erc20.BalanceOf(&bind.CallOpts{Context: ctx}, testWithdrawalAddress) if err != nil { - return err + return fmt.Errorf("could not get balance %w", err) + } + + expectedBalance := new(big.Int).Add(startBalance, withdrawalAmount) + + if balance.Cmp(expectedBalance) != 0 { + return fmt.Errorf("balance not updated") + } + + return nil + }) + c.Require().NoError(err) +} + +func (c *RelayerClientSuite) TestEthWithdrawCLI() { + res, err := c.Client.Withdraw(c.GetTestContext(), &relapi.WithdrawRequest{ + ChainID: c.underlying.originChainID, + To: common.HexToAddress(testWithdrawalAddress.String()), + Amount: "1000000000000000000", + TokenAddress: chain.EthAddress, + }) + c.Require().NoError(err) + + // Wait for the transaction to be mined + err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + status, err := c.underlying.database.SubmitterDB(). + GetNonceStatus( + ctx, + c.underlying.wallet.Address(), + big.NewInt(int64(c.underlying.originChainID)), + res.Nonce, + ) + if err != nil { + return fmt.Errorf("could not get status %w", err) + } + + if status != submitterdb.Stored { + return fmt.Errorf("transaction not mined") + } + + return nil + }) + c.Require().NoError(err) + c.Require().NotNil(res) +} + +func (c *RelayerClientSuite) TestERC20WithdrawCLI() { + backend := c.underlying.testBackends[uint64(c.underlying.originChainID)] + + _, erc20 := c.underlying.deployManager.GetMockERC20(c.GetTestContext(), backend) + + startBalance, err := erc20.BalanceOf(&bind.CallOpts{Context: c.GetTestContext()}, testWithdrawalAddress) + c.Require().NoError(err) + + withdrawalAmount := big.NewInt(1000000000000000000) + + res, err := c.Client.Withdraw(c.GetTestContext(), &relapi.WithdrawRequest{ + ChainID: c.underlying.originChainID, + To: common.HexToAddress(testWithdrawalAddress.String()), + Amount: withdrawalAmount.String(), + TokenAddress: erc20.Address(), + }) + c.Require().NoError(err) + + // Wait for the transaction to be mined + err = retry.WithBackoff(c.GetTestContext(), func(ctx context.Context) error { + balance, err := erc20.BalanceOf(&bind.CallOpts{Context: ctx}, testWithdrawalAddress) + if err != nil { + return fmt.Errorf("could not fetch balance %w", err) } expectedBalance := new(big.Int).Add(startBalance, withdrawalAmount) @@ -272,5 +341,7 @@ func (c *RelayerClientSuite) TestERC20Withdraw() { return nil }) + c.Require().NoError(err) + c.Require().NotNil(res) } diff --git a/services/rfq/relayer/relapi/suite_test.go b/services/rfq/relayer/relapi/suite_test.go index 237d4f6a36..f7fd89532e 100644 --- a/services/rfq/relayer/relapi/suite_test.go +++ b/services/rfq/relayer/relapi/suite_test.go @@ -2,14 +2,15 @@ package relapi_test import ( "fmt" - "github.com/ethereum/go-ethereum/params" - "github.com/synapsecns/sanguine/services/rfq/relayer/chain" - "github.com/synapsecns/sanguine/services/rfq/testutil" "math/big" "strconv" "sync" "testing" + "github.com/ethereum/go-ethereum/params" + "github.com/synapsecns/sanguine/services/rfq/relayer/chain" + "github.com/synapsecns/sanguine/services/rfq/testutil" + "github.com/Flaque/filet" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -133,7 +134,6 @@ func (c *RelayerServerSuite) SetupTest() { }() for _, backend := range c.testBackends { - backend := backend go func() { defer wg.Done() backend.FundAccount(c.GetTestContext(), c.wallet.Address(), *big.NewInt(params.Ether)) @@ -177,6 +177,7 @@ func (c *RelayerServerSuite) SetupSuite() { g, _ := errgroup.WithContext(c.GetSuiteContext()) for _, chainID := range chainIDs { + chainID := chainID // Setup Anvil backend for the suite to have RPC support g.Go(func() error { backend := geth.NewEmbeddedBackendForChainID(c.GetSuiteContext(), c.T(), new(big.Int).SetUint64(chainID)) From a3c3516450549963596ec9eb08690aa4913bac58 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Tue, 2 Jul 2024 10:40:46 -0400 Subject: [PATCH 06/10] add tests --- services/rfq/relayer/relapi/export_test.go | 11 +++++ services/rfq/relayer/relapi/server_test.go | 52 ++++++++++++++++++++++ services/rfq/relayer/relconfig/config.go | 8 ++-- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 services/rfq/relayer/relapi/export_test.go diff --git a/services/rfq/relayer/relapi/export_test.go b/services/rfq/relayer/relapi/export_test.go new file mode 100644 index 0000000000..038ce921a5 --- /dev/null +++ b/services/rfq/relayer/relapi/export_test.go @@ -0,0 +1,11 @@ +package relapi + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" +) + +// TokenIDExists checks if a token ID exists in the config. +func TokenIDExists(cfg relconfig.Config, tokenAddress common.Address, chainID int) bool { + return tokenIDExists(cfg, tokenAddress, chainID) +} diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 256a8f42ca..6f8a4285da 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -5,9 +5,12 @@ import ( "encoding/json" "fmt" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/assert" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "math/big" "net/http" + "testing" "time" "github.com/ethereum/go-ethereum/common" @@ -274,3 +277,52 @@ func (c *RelayerClientSuite) TestERC20Withdraw() { }) c.Require().NoError(err) } + +func TestTokenIDExists(t *testing.T) { + cfg := relconfig.Config{ + QuotableTokens: map[string][]string{ + fmt.Sprintf("1%s0x1234567890abcdef1234567890abcdef12345678", relconfig.TokenIDDelimiter): {}, + fmt.Sprintf("1%s0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", relconfig.TokenIDDelimiter): {}, + }, + } + + tests := []struct { + name string + tokenAddress common.Address + chainID int + expected bool + }{ + { + name: "Valid token address", + tokenAddress: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), + chainID: 1, + expected: true, + }, + { + name: "Invalid token address", + tokenAddress: common.HexToAddress("0x0000000000000000000000000000000000000000"), + chainID: 1, + expected: false, + }, + { + name: "Valid token address, different chain ID", + tokenAddress: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + chainID: 2, + expected: false, + }, + { + name: "Edge case: empty token address", + tokenAddress: common.Address{}, + chainID: 1, + expected: false, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + result := relapi.TokenIDExists(cfg, tt.tokenAddress, tt.chainID) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 00055b11e1..6711e8b2ac 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -142,12 +142,12 @@ type FeePricerConfig struct { HTTPTimeoutMs int `yaml:"http_timeout_ms"` } -const tokenIDDelimiter = "-" +const TokenIDDelimiter = "-" // SanitizeTokenID takes a raw string, makes sure it is a valid token ID, // and returns the token ID as string with a checksummed address. func SanitizeTokenID(id string) (sanitized string, err error) { - split := strings.Split(id, tokenIDDelimiter) + split := strings.Split(id, TokenIDDelimiter) if len(split) != 2 { return sanitized, fmt.Errorf("invalid token ID: %s", id) } @@ -156,7 +156,7 @@ func SanitizeTokenID(id string) (sanitized string, err error) { return sanitized, fmt.Errorf("invalid chain ID: %s", split[0]) } addr := common.HexToAddress(split[1]) - sanitized = fmt.Sprintf("%d%s%s", chainID, tokenIDDelimiter, addr.Hex()) + sanitized = fmt.Sprintf("%d%s%s", chainID, TokenIDDelimiter, addr.Hex()) return sanitized, nil } @@ -168,7 +168,7 @@ func DecodeTokenID(id string) (chainID int, addr common.Address, err error) { return chainID, addr, err } - split := strings.Split(id, tokenIDDelimiter) + split := strings.Split(id, TokenIDDelimiter) if len(split) != 2 { return chainID, addr, fmt.Errorf("invalid token ID: %s", id) } From 6d10be7c9e106ed4c7a4d04bfcb289af43e52e9f Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Tue, 2 Jul 2024 10:47:40 -0400 Subject: [PATCH 07/10] add decode test --- services/rfq/relayer/relconfig/config.go | 4 ++ services/rfq/relayer/relconfig/config_test.go | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index 1ac5ccbe0f..d0cdce49c6 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -177,6 +177,10 @@ func DecodeTokenID(id string) (chainID int, addr common.Address, err error) { if err != nil { return chainID, addr, fmt.Errorf("invalid chain ID: %s", split[0]) } + if !common.IsHexAddress(split[1]) { + return chainID, addr, fmt.Errorf("invalid address: %s", split[1]) + } + addr = common.HexToAddress(split[1]) return chainID, addr, nil } diff --git a/services/rfq/relayer/relconfig/config_test.go b/services/rfq/relayer/relconfig/config_test.go index c3fb89fb52..5dc5ba9e1b 100644 --- a/services/rfq/relayer/relconfig/config_test.go +++ b/services/rfq/relayer/relconfig/config_test.go @@ -456,3 +456,49 @@ func TestValidation(t *testing.T) { assert.Nil(t, err) }) } + +func TestDecodeTokenID(t *testing.T) { + tests := []struct { + name string + id string + wantChain int + wantAddr common.Address + wantErr bool + }{ + { + name: "valid token ID", + id: "1-0x1234567890abcdef1234567890abcdef12345678", + wantChain: 1, + wantAddr: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), + wantErr: false, + }, + { + name: "invalid token ID format", + id: "1_0x1234567890abcdef1234567890abcdef12345678", + wantErr: true, + }, + { + name: "invalid chain ID", + id: "x-0x1234567890abcdef1234567890abcdef12345678", + wantErr: true, + }, + { + name: "invalid address", + id: "1-0x12345", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotChain, gotAddr, err := relconfig.DecodeTokenID(tt.id) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantChain, gotChain) + assert.Equal(t, tt.wantAddr, gotAddr) + } + }) + } +} From dce9713849eecd950cd365c4bdf9684a4b594c21 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Tue, 2 Jul 2024 10:57:19 -0400 Subject: [PATCH 08/10] address lint issue from 5ed6d9a306afa89a55e9768a3a1054e4b6147227 --- services/rfq/relayer/relconfig/config_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/rfq/relayer/relconfig/config_test.go b/services/rfq/relayer/relconfig/config_test.go index 5dc5ba9e1b..01a94286d4 100644 --- a/services/rfq/relayer/relconfig/config_test.go +++ b/services/rfq/relayer/relconfig/config_test.go @@ -489,7 +489,8 @@ func TestDecodeTokenID(t *testing.T) { }, } - for _, tt := range tests { + for i := range tests { + tt := tests[i] t.Run(tt.name, func(t *testing.T) { gotChain, gotAddr, err := relconfig.DecodeTokenID(tt.id) if tt.wantErr { From 2965bb7b09f595cc8dfa35dc2969ba5d7ed1b8c5 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Tue, 2 Jul 2024 11:05:30 -0400 Subject: [PATCH 09/10] whitelist test --- services/rfq/relayer/relapi/export_test.go | 5 ++++ services/rfq/relayer/relapi/server_test.go | 33 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/services/rfq/relayer/relapi/export_test.go b/services/rfq/relayer/relapi/export_test.go index 038ce921a5..b10a72fc24 100644 --- a/services/rfq/relayer/relapi/export_test.go +++ b/services/rfq/relayer/relapi/export_test.go @@ -9,3 +9,8 @@ import ( func TokenIDExists(cfg relconfig.Config, tokenAddress common.Address, chainID int) bool { return tokenIDExists(cfg, tokenAddress, chainID) } + +// ToAddressIsWhitelisted checks if a to address is whitelisted. +func ToAddressIsWhitelisted(cfg relconfig.Config, to common.Address) bool { + return toAddressIsWhitelisted(cfg, to) +} diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 63114153be..21544fbc0c 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -327,6 +327,39 @@ func TestTokenIDExists(t *testing.T) { } } +func TestToAddressIsWhitelisted(t *testing.T) { + cfg := relconfig.Config{ + WithdrawalWhitelist: []string{ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + }, + } + + tests := []struct { + name string + toAddress common.Address + expected bool + }{ + { + name: "Address is whitelisted", + toAddress: common.HexToAddress("0x1111111111111111111111111111111111111111"), + expected: true, + }, + { + name: "Address is not whitelisted", + toAddress: common.HexToAddress("0x3333333333333333333333333333333333333333"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := relapi.ToAddressIsWhitelisted(cfg, tt.toAddress) + assert.Equal(t, tt.expected, result) + }) + } +} + func (c *RelayerClientSuite) TestEthWithdrawCLI() { res, err := c.Client.Withdraw(c.GetTestContext(), &relapi.WithdrawRequest{ ChainID: c.underlying.originChainID, From 5c4554a47c71aac917075adc1a8501baed9dbc85 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Tue, 2 Jul 2024 11:35:08 -0400 Subject: [PATCH 10/10] scope lint [goreleaser] --- services/rfq/relayer/relapi/server_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/rfq/relayer/relapi/server_test.go b/services/rfq/relayer/relapi/server_test.go index 21544fbc0c..e4af4b116c 100644 --- a/services/rfq/relayer/relapi/server_test.go +++ b/services/rfq/relayer/relapi/server_test.go @@ -352,7 +352,8 @@ func TestToAddressIsWhitelisted(t *testing.T) { }, } - for _, tt := range tests { + for i := range tests { + tt := tests[i] t.Run(tt.name, func(t *testing.T) { result := relapi.ToAddressIsWhitelisted(cfg, tt.toAddress) assert.Equal(t, tt.expected, result)