From a836022a006221d25d4a86de4508138ba9780469 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 11:23:34 -0500 Subject: [PATCH 01/20] change tx price oracle to be slightly more chain agnostic, pull config when getting gas cost instead of passing in coin gecko id --- shared/evmrpc/tx_price_oracle.go | 61 +++++++++++++++++--------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/shared/evmrpc/tx_price_oracle.go b/shared/evmrpc/tx_price_oracle.go index 4a0fa29..2ff850e 100644 --- a/shared/evmrpc/tx_price_oracle.go +++ b/shared/evmrpc/tx_price_oracle.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/skip-mev/go-fast-solver/shared/clients/coingecko" + "github.com/skip-mev/go-fast-solver/shared/config" ) const ( @@ -26,7 +27,7 @@ func NewOracle(coingecko coingecko.PriceClient) *Oracle { // TxFeeUUSDC estimates what the cost in uusdc would be to execute a tx. The // tx's gas fee cap and gas limit must be set. -func (o *Oracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction, gasTokenCoingeckoID string) (*big.Int, error) { +func (o *Oracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction) (*big.Int, error) { if tx.Type() != types.DynamicFeeTxType { return nil, fmt.Errorf("tx type must be dynamic fee tx, got %d", tx.Type()) } @@ -40,49 +41,53 @@ func (o *Oracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction, gasToken // for a dry ran tx, Gas() will be the result of calling eth_estimateGas estimatedGasUsed := tx.Gas() - return o.gasCostUUSDC(ctx, estimatedPricePerGas, big.NewInt(int64(estimatedGasUsed)), gasTokenCoingeckoID) + return o.GasCostUUSDC(ctx, estimatedPricePerGas, big.NewInt(int64(estimatedGasUsed)), tx.ChainId().String()) } // gasCostUUSDC converts an amount of gas and the price per gas in gwei to // uusdc based on the current CoinGecko price of ethereum in usd. -func (o *Oracle) gasCostUUSDC(ctx context.Context, pricePerGasWei *big.Int, gasUsed *big.Int, gasTokenCoingeckoID string) (*big.Int, error) { - // Calculate transaction fee in Wei - txFeeWei := new(big.Int).Mul(gasUsed, pricePerGasWei) +func (o *Oracle) GasCostUUSDC(ctx context.Context, pricePerGas *big.Int, gasUsed *big.Int, chainID string) (*big.Int, error) { + chainConfig, err := config.GetConfigReader(ctx).GetChainConfig(chainID) + if err != nil { + return nil, fmt.Errorf("getting config for chain %s: %w", chainID, err) + } + + // Calculate transaction fee + txFee := new(big.Int).Mul(gasUsed, pricePerGas) - // Get the ETH price in USD cents from CoinGecko - ethPriceUSD, err := o.coingecko.GetSimplePrice(ctx, gasTokenCoingeckoID, coingeckoUSDCurrency) + // Get the gas token price in USD cents from CoinGecko + gasTokenPriceUSD, err := o.coingecko.GetSimplePrice(ctx, chainConfig.GasTokenCoingeckoID, coingeckoUSDCurrency) if err != nil { - return nil, fmt.Errorf("getting CoinGecko price of Ethereum in USD: %w", err) + return nil, fmt.Errorf("getting CoinGecko price of %s in USD: %w", chainConfig.GasTokenCoingeckoID, err) } - // Convert ETH price to microunits of USDC (uusdc) per Wei - // WEI_PER_ETH = 1_000_000_000_000_000_000 (1 ETH = 10^18 Wei) + // conversion to bring gas token to its smallest representation // UUSDC_PER_USD = 1_000_000 (1 USD = 10^6 UUSDC) - const WEI_PER_ETH = 1_000_000_000_000_000_000 + smallestGasTokenConversion := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(chainConfig.GasTokenDecimals)), nil) const UUSDC_PER_USD = 1_000_000 // convert eth price in usd to eth price in uusdc - ethPriceUUSDC := new(big.Float).Mul(big.NewFloat(ethPriceUSD), new(big.Float).SetInt64(UUSDC_PER_USD)) + gasTokenPriceUUSDC := new(big.Float).Mul(big.NewFloat(gasTokenPriceUSD), new(big.Float).SetInt64(UUSDC_PER_USD)) - // eth price in usd comes back from coin gecko with two decimals. Since we - // just converted to uusdc, shifting the decimal place right by 6, we can - // safely turn this into an int now - ethPriceUUSDCInt, ok := new(big.Int).SetString(ethPriceUUSDC.String(), 10) + // gas token price in usd comes back from coin gecko with two decimals. + // Since we just converted to uusdc, shifting the decimal place right by 6, + // we can safely turn this into an int now + gasTokenPriceUUSDCInt, ok := new(big.Int).SetString(gasTokenPriceUUSDC.String(), 10) if !ok { - return nil, fmt.Errorf("converting eth price in uusdc %s to *big.Int", ethPriceUUSDC.String()) + return nil, fmt.Errorf("converting %s price in uusdc %s to *big.Int", chainConfig.GasTokenCoingeckoID, gasTokenPriceUUSDC.String()) } // What we are really trying to do is: - // eth price uusdc / wei per eth = wei price in uusdc - // wei price in uusdc * tx fee wei = tx fee uusdc - // However we are choosing to first multiply eth price uusdc by tx fee wei - // so that we can do integer division when converting to wei, since if we - // first do integer division (before multiplying), we are going to cut off - // necessary decimals. there are limits of this, if eth price uusdc * tx - // fee wei has less than 9 digits, then we will just return 0. However, - // this is unlikely in practice and the tx fee would be very small if this - // is the case. - tmp := new(big.Int).Mul(ethPriceUUSDCInt, txFeeWei) - txFeeUUSDC := new(big.Int).Div(tmp, big.NewInt(WEI_PER_ETH)) + // gas token price uusdc / smallest gas token conversion = smallest gas token representation price in uusdc + // smallest gas token representation price in uusdc * tx fee = tx fee uusdc + // However we are choosing to first multiply gas token price uusdc by tx + // fee so that we can do integer division when converting to smallest + // representation, since if we first do integer division (before + // multiplying), we are going to cut off necessary decimals. there are + // limits of this, if gas token price uusdc * tx fee has less than 9 + // digits, then we will just return 0. However, this is unlikely in + // practice and the tx fee would be very small if this is the case. + tmp := new(big.Int).Mul(gasTokenPriceUUSDCInt, txFee) + txFeeUUSDC := new(big.Int).Div(tmp, smallestGasTokenConversion) return txFeeUUSDC, nil } From 526f1cae63ab9a0c6475be99e7eb101773e42029 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:44:10 -0500 Subject: [PATCH 02/20] add tx cost to submitted txs table and update queries --- db/gen/db/models.go | 1 + db/gen/db/transactions.sql.go | 22 ++++++++++++++----- .../000004_add_submitted_tx_table.up.sql | 2 +- ...price_cost_column_to_submitted_tx.down.sql | 1 + ...x_price_cost_column_to_submitted_tx.up.sql | 1 + db/queries/transactions.sql | 4 +++- 6 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql create mode 100644 db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql diff --git a/db/gen/db/models.go b/db/gen/db/models.go index edef81a..001a74e 100644 --- a/db/gen/db/models.go +++ b/db/gen/db/models.go @@ -86,6 +86,7 @@ type SubmittedTx struct { TxType string TxStatus string TxStatusMessage sql.NullString + TxCostUusdc sql.NullString } type TransferMonitorMetadatum struct { diff --git a/db/gen/db/transactions.sql.go b/db/gen/db/transactions.sql.go index a142b8b..d60379a 100644 --- a/db/gen/db/transactions.sql.go +++ b/db/gen/db/transactions.sql.go @@ -11,7 +11,7 @@ import ( ) const getSubmittedTxsByHyperlaneTransferId = `-- name: GetSubmittedTxsByHyperlaneTransferId :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message FROM submitted_txs WHERE hyperlane_transfer_id = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE hyperlane_transfer_id = ? ` func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hyperlaneTransferID sql.NullInt64) ([]SubmittedTx, error) { @@ -36,6 +36,7 @@ func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hype &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ); err != nil { return nil, err } @@ -51,7 +52,7 @@ func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hype } const getSubmittedTxsByOrderIdAndType = `-- name: GetSubmittedTxsByOrderIdAndType :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message FROM submitted_txs WHERE order_id = ? AND tx_type = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE order_id = ? AND tx_type = ? ` type GetSubmittedTxsByOrderIdAndTypeParams struct { @@ -81,6 +82,7 @@ func (q *Queries) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSu &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ); err != nil { return nil, err } @@ -96,7 +98,7 @@ func (q *Queries) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSu } const getSubmittedTxsByOrderStatusAndType = `-- name: GetSubmittedTxsByOrderStatusAndType :many -SELECT submitted_txs.id, submitted_txs.created_at, submitted_txs.updated_at, submitted_txs.order_id, submitted_txs.order_settlement_id, submitted_txs.hyperlane_transfer_id, submitted_txs.chain_id, submitted_txs.tx_hash, submitted_txs.raw_tx, submitted_txs.tx_type, submitted_txs.tx_status, submitted_txs.tx_status_message FROM submitted_txs INNER JOIN orders on submitted_txs.order_id = orders.id WHERE orders.order_status = ? AND submitted_txs.tx_type = ? +SELECT submitted_txs.id, submitted_txs.created_at, submitted_txs.updated_at, submitted_txs.order_id, submitted_txs.order_settlement_id, submitted_txs.hyperlane_transfer_id, submitted_txs.chain_id, submitted_txs.tx_hash, submitted_txs.raw_tx, submitted_txs.tx_type, submitted_txs.tx_status, submitted_txs.tx_status_message, submitted_txs.tx_cost_uusdc FROM submitted_txs INNER JOIN orders on submitted_txs.order_id = orders.id WHERE orders.order_status = ? AND submitted_txs.tx_type = ? ` type GetSubmittedTxsByOrderStatusAndTypeParams struct { @@ -126,6 +128,7 @@ func (q *Queries) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg G &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ); err != nil { return nil, err } @@ -141,7 +144,7 @@ func (q *Queries) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg G } const getSubmittedTxsWithStatus = `-- name: GetSubmittedTxsWithStatus :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message FROM submitted_txs WHERE tx_status = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE tx_status = ? ` func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string) ([]SubmittedTx, error) { @@ -166,6 +169,7 @@ func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ); err != nil { return nil, err } @@ -181,7 +185,7 @@ func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string } const insertSubmittedTx = `-- name: InsertSubmittedTx :one -INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message +INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc ` type InsertSubmittedTxParams struct { @@ -220,17 +224,21 @@ func (q *Queries) InsertSubmittedTx(ctx context.Context, arg InsertSubmittedTxPa &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ) return i, err } const setSubmittedTxStatus = `-- name: SetSubmittedTxStatus :one -UPDATE submitted_txs SET tx_status = ?, tx_status_message = ?, updated_at = CURRENT_TIMESTAMP WHERE tx_hash = ? AND chain_id = ? RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message +UPDATE submitted_txs SET + tx_status = ?, tx_status_message = ?, tx_cost_uusdc = ?, updated_at = CURRENT_TIMESTAMP +WHERE tx_hash = ? AND chain_id = ? RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc ` type SetSubmittedTxStatusParams struct { TxStatus string TxStatusMessage sql.NullString + TxCostUusdc sql.NullString TxHash string ChainID string } @@ -239,6 +247,7 @@ func (q *Queries) SetSubmittedTxStatus(ctx context.Context, arg SetSubmittedTxSt row := q.db.QueryRowContext(ctx, setSubmittedTxStatus, arg.TxStatus, arg.TxStatusMessage, + arg.TxCostUusdc, arg.TxHash, arg.ChainID, ) @@ -256,6 +265,7 @@ func (q *Queries) SetSubmittedTxStatus(ctx context.Context, arg SetSubmittedTxSt &i.TxType, &i.TxStatus, &i.TxStatusMessage, + &i.TxCostUusdc, ) return i, err } diff --git a/db/migrations/000004_add_submitted_tx_table.up.sql b/db/migrations/000004_add_submitted_tx_table.up.sql index 5f3c062..734e598 100644 --- a/db/migrations/000004_add_submitted_tx_table.up.sql +++ b/db/migrations/000004_add_submitted_tx_table.up.sql @@ -25,4 +25,4 @@ WHERE order_settlement_id IS NOT NULL; CREATE UNIQUE INDEX submitted_txs_chain_tx_key ON submitted_txs(chain_id, tx_hash) -WHERE order_settlement_id IS NULL; \ No newline at end of file +WHERE order_settlement_id IS NULL; diff --git a/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql b/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql new file mode 100644 index 0000000..8c1bfb5 --- /dev/null +++ b/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql @@ -0,0 +1 @@ +ALTER TABLE submitted_txs DROP COLUMN tx_cost_uusdc; diff --git a/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql b/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql new file mode 100644 index 0000000..565adc9 --- /dev/null +++ b/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql @@ -0,0 +1 @@ +ALTER TABLE submitted_txs ADD tx_cost_uusdc TEXT; diff --git a/db/queries/transactions.sql b/db/queries/transactions.sql index be825db..560cccc 100644 --- a/db/queries/transactions.sql +++ b/db/queries/transactions.sql @@ -11,7 +11,9 @@ SELECT * FROM submitted_txs WHERE hyperlane_transfer_id = ?; SELECT * FROM submitted_txs WHERE tx_status = ?; -- name: SetSubmittedTxStatus :one -UPDATE submitted_txs SET tx_status = ?, tx_status_message = ?, updated_at = CURRENT_TIMESTAMP WHERE tx_hash = ? AND chain_id = ? RETURNING *; +UPDATE submitted_txs SET + tx_status = ?, tx_status_message = ?, tx_cost_uusdc = ?, updated_at = CURRENT_TIMESTAMP +WHERE tx_hash = ? AND chain_id = ? RETURNING *; -- name: GetSubmittedTxsByOrderStatusAndType :many SELECT submitted_txs.* FROM submitted_txs INNER JOIN orders on submitted_txs.order_id = orders.id WHERE orders.order_status = ? AND submitted_txs.tx_type = ?; From d000b5fe0c212810bf8534558463cd946440bbcd Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:45:42 -0500 Subject: [PATCH 03/20] move evmrpc tx price oracle to chain agnostic oracle package and allow users to call gas cost uusdc --- shared/{evmrpc => oracle}/tx_price_oracle.go | 15 +++++++-------- .../{evmrpc => oracle}/tx_price_oracle_test.go | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) rename shared/{evmrpc => oracle}/tx_price_oracle.go (88%) rename shared/{evmrpc => oracle}/tx_price_oracle_test.go (77%) diff --git a/shared/evmrpc/tx_price_oracle.go b/shared/oracle/tx_price_oracle.go similarity index 88% rename from shared/evmrpc/tx_price_oracle.go rename to shared/oracle/tx_price_oracle.go index 2ff850e..ab48b61 100644 --- a/shared/evmrpc/tx_price_oracle.go +++ b/shared/oracle/tx_price_oracle.go @@ -1,4 +1,4 @@ -package evmrpc +package oracle import ( "context" @@ -41,20 +41,19 @@ func (o *Oracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction) (*big.In // for a dry ran tx, Gas() will be the result of calling eth_estimateGas estimatedGasUsed := tx.Gas() - return o.GasCostUUSDC(ctx, estimatedPricePerGas, big.NewInt(int64(estimatedGasUsed)), tx.ChainId().String()) + + txFee := new(big.Int).Mul(estimatedPricePerGas, big.NewInt(int64(estimatedGasUsed))) + return o.GasCostUUSDC(ctx, txFee, tx.ChainId().String()) } -// gasCostUUSDC converts an amount of gas and the price per gas in gwei to -// uusdc based on the current CoinGecko price of ethereum in usd. -func (o *Oracle) GasCostUUSDC(ctx context.Context, pricePerGas *big.Int, gasUsed *big.Int, chainID string) (*big.Int, error) { +// GasCostUUSDC converts a tx fee to uusdc based on the current CoinGecko of +// the gas token in usd. +func (o *Oracle) GasCostUUSDC(ctx context.Context, txFee *big.Int, chainID string) (*big.Int, error) { chainConfig, err := config.GetConfigReader(ctx).GetChainConfig(chainID) if err != nil { return nil, fmt.Errorf("getting config for chain %s: %w", chainID, err) } - // Calculate transaction fee - txFee := new(big.Int).Mul(gasUsed, pricePerGas) - // Get the gas token price in USD cents from CoinGecko gasTokenPriceUSD, err := o.coingecko.GetSimplePrice(ctx, chainConfig.GasTokenCoingeckoID, coingeckoUSDCurrency) if err != nil { diff --git a/shared/evmrpc/tx_price_oracle_test.go b/shared/oracle/tx_price_oracle_test.go similarity index 77% rename from shared/evmrpc/tx_price_oracle_test.go rename to shared/oracle/tx_price_oracle_test.go index 16427a7..95770ab 100644 --- a/shared/evmrpc/tx_price_oracle_test.go +++ b/shared/oracle/tx_price_oracle_test.go @@ -1,4 +1,4 @@ -package evmrpc_test +package oracle_test import ( "context" @@ -7,7 +7,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/skip-mev/go-fast-solver/mocks/shared/clients/coingecko" - "github.com/skip-mev/go-fast-solver/shared/evmrpc" + "github.com/skip-mev/go-fast-solver/shared/config" + "github.com/skip-mev/go-fast-solver/shared/oracle" "github.com/stretchr/testify/assert" ) @@ -45,7 +46,14 @@ func Test_Oracle_TxFeeUUSDC(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { ctx := context.Background() + cfg := config.Config{ + Chains: map[string]config.ChainConfig{ + "ethereum": {ChainID: "1", GasTokenSymbol: "ETH", GasTokenDecimals: 18, GasTokenCoingeckoID: "ethereum"}, + }, + } + ctx = config.ConfigReaderContext(ctx, config.NewConfigReader(cfg)) tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(1), // max wei paid per gas GasFeeCap: big.NewInt(int64(tt.MaxPricePerGas)), // total gas used @@ -55,8 +63,8 @@ func Test_Oracle_TxFeeUUSDC(t *testing.T) { mockcoingecko := coingecko.NewMockPriceClient(t) mockcoingecko.EXPECT().GetSimplePrice(ctx, "ethereum", "usd").Return(tt.ETHPriceUSD, nil) - oracle := evmrpc.NewOracle(mockcoingecko) - uusdcPrice, err := oracle.TxFeeUUSDC(ctx, tx, "ethereum") + oracle := oracle.NewOracle(mockcoingecko) + uusdcPrice, err := oracle.TxFeeUUSDC(ctx, tx) assert.NoError(t, err) assert.Equal(t, tt.ExpectedUUSDCPrice, uusdcPrice.Int64()) }) From c7cabfeba64a377baa357ce74fc18c2b0d23240e Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:45:56 -0500 Subject: [PATCH 04/20] parse fee out of tx result to use as gas cost --- shared/bridges/cctp/cosmos_bridge_client.go | 37 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/shared/bridges/cctp/cosmos_bridge_client.go b/shared/bridges/cctp/cosmos_bridge_client.go index f7332eb..2904ab4 100644 --- a/shared/bridges/cctp/cosmos_bridge_client.go +++ b/shared/bridges/cctp/cosmos_bridge_client.go @@ -7,13 +7,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/skip-mev/go-fast-solver/shared/contracts/fast_transfer_gateway" - "github.com/skip-mev/go-fast-solver/shared/txexecutor/cosmos" "math/big" "strconv" "strings" "time" + "github.com/skip-mev/go-fast-solver/shared/contracts/fast_transfer_gateway" + "github.com/skip-mev/go-fast-solver/shared/txexecutor/cosmos" + sdkgrpc "github.com/cosmos/cosmos-sdk/types/grpc" "google.golang.org/grpc/metadata" @@ -166,11 +167,37 @@ func (c *CosmosBridgeClient) GetTxResult(ctx context.Context, txHash string) (*b result, err := c.rpcClient.Tx(ctx, txHashBytes, false) if err != nil { return nil, nil, err - } else if result.TxResult.Code != 0 { - return big.NewInt(result.TxResult.GasUsed), &TxFailure{fmt.Sprintf("tx failed with code: %d and log: %s", result.TxResult.Code, result.TxResult.Log)}, nil } - return big.NewInt(result.TxResult.GasUsed), nil, nil + // parse tx fee event and use as the gas cost. we are using the fee event + // as the gas cost, technically this is not always true for all cosmos + // txns, however for all of the transactions that the solver will submit + // and get results of via this function, this should be true + var fee sdk.Coin +outer: + for _, event := range result.TxResult.GetEvents() { + if event.GetType() != "tx" { + continue + } + + for _, attribute := range event.GetAttributes() { + if attribute.GetKey() != "fee" { + continue + } + + coin, err := sdk.ParseCoinNormalized(attribute.GetValue()) + if err != nil { + return nil, nil, fmt.Errorf("parsing coin from tx fee event attribute %s: %w", attribute.GetValue(), err) + } + fee = coin + break outer + } + } + + if result.TxResult.Code != 0 { + return fee.Amount.BigInt(), &TxFailure{fmt.Sprintf("tx failed with code: %d and log: %s", result.TxResult.Code, result.TxResult.Log)}, nil + } + return fee.Amount.BigInt(), nil, nil } func (c *CosmosBridgeClient) IsSettlementComplete(ctx context.Context, gatewayContractAddress, orderID string) (bool, error) { From fb3d9e5d29f5a57569c064aeba9ae1ce438b87f8 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:47:04 -0500 Subject: [PATCH 05/20] rename evm tx price oracle -> price oracle --- cmd/solver/main.go | 10 ++++++---- cmd/solvercli/cmd/relay.go | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/solver/main.go b/cmd/solver/main.go index 8e833ed..9b902c1 100644 --- a/cmd/solver/main.go +++ b/cmd/solver/main.go @@ -4,11 +4,13 @@ import ( "context" "flag" "fmt" - "github.com/skip-mev/go-fast-solver/gasmonitor" "os/signal" "syscall" "time" + "github.com/skip-mev/go-fast-solver/gasmonitor" + + "github.com/skip-mev/go-fast-solver/shared/oracle" "github.com/skip-mev/go-fast-solver/shared/txexecutor/cosmos" "github.com/skip-mev/go-fast-solver/shared/txexecutor/evm" @@ -98,9 +100,9 @@ func main() { rateLimitedClient := utils.DefaultRateLimitedHTTPClient(3) coingeckoClient := coingecko.NewCoingeckoClient(rateLimitedClient, "https://api.coingecko.com/api/v3/", "") cachedCoinGeckoClient := coingecko.NewCachedPriceClient(coingeckoClient, 15*time.Minute) - evmTxPriceOracle := evmrpc.NewOracle(cachedCoinGeckoClient) + txPriceOracle := oracle.NewOracle(cachedCoinGeckoClient) - hype, err := hyperlane.NewMultiClientFromConfig(ctx, evmManager, keyStore, evmTxPriceOracle, evmTxExecutor) + hype, err := hyperlane.NewMultiClientFromConfig(ctx, evmManager, keyStore, txPriceOracle, evmTxExecutor) if err != nil { lmt.Logger(ctx).Fatal("creating hyperlane multi client from config", zap.Error(err)) } @@ -154,7 +156,7 @@ func main() { }) eg.Go(func() error { - r, err := txverifier.NewTxVerifier(ctx, db.New(dbConn), clientManager) + r, err := txverifier.NewTxVerifier(ctx, db.New(dbConn), clientManager, txPriceOracle) if err != nil { return err } diff --git a/cmd/solvercli/cmd/relay.go b/cmd/solvercli/cmd/relay.go index 9559652..c8c9f72 100644 --- a/cmd/solvercli/cmd/relay.go +++ b/cmd/solvercli/cmd/relay.go @@ -7,6 +7,7 @@ import ( "encoding/json" "time" + "github.com/skip-mev/go-fast-solver/shared/oracle" "github.com/skip-mev/go-fast-solver/shared/txexecutor/evm" "os/signal" @@ -97,9 +98,9 @@ var relayCmd = &cobra.Command{ rateLimitedClient := utils.DefaultRateLimitedHTTPClient(3) coingeckoClient := coingecko.NewCoingeckoClient(rateLimitedClient, "https://api.coingecko.com/api/v3/", "") cachedCoinGeckoClient := coingecko.NewCachedPriceClient(coingeckoClient, 15*time.Minute) - evmTxPriceOracle := evmrpc.NewOracle(cachedCoinGeckoClient) + txPriceOracle := oracle.NewOracle(cachedCoinGeckoClient) evmTxExecutor := evm.DefaultEVMTxExecutor() - hype, err := hyperlane.NewMultiClientFromConfig(ctx, evmrpc.NewEVMRPCClientManager(), keyStore, evmTxPriceOracle, evmTxExecutor) + hype, err := hyperlane.NewMultiClientFromConfig(ctx, evmrpc.NewEVMRPCClientManager(), keyStore, txPriceOracle, evmTxExecutor) if err != nil { lmt.Logger(ctx).Error("Error creating hyperlane multi client from config", zap.Error(err)) } From 61c4a6c9915dff9e06dde6b81403d7dbdac58254 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:47:22 -0500 Subject: [PATCH 06/20] update interface for calling tx price oracle from hyperlane client --- hyperlane/ethereum/client.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hyperlane/ethereum/client.go b/hyperlane/ethereum/client.go index eca3441..72a804d 100644 --- a/hyperlane/ethereum/client.go +++ b/hyperlane/ethereum/client.go @@ -28,7 +28,7 @@ import ( ) type TxPriceOracle interface { - TxFeeUUSDC(ctx context.Context, tx *ethtypes.Transaction, gasTokenCoingeckoID string) (*big.Int, error) + TxFeeUUSDC(ctx context.Context, tx *ethtypes.Transaction) (*big.Int, error) } type HyperlaneClient struct { @@ -279,10 +279,6 @@ func (c *HyperlaneClient) QuoteProcessUUSDC(ctx context.Context, domain string, if err != nil { return nil, fmt.Errorf("getting chainID for hyperlane domain %s: %w", domain, err) } - destinationChainConfig, err := config.GetConfigReader(ctx).GetChainConfig(destinationChainID) - if err != nil { - return nil, fmt.Errorf("getting destination chain %s config: %w", destinationChainID, err) - } signer, err := c.signer(ctx, domain) if err != nil { @@ -309,7 +305,7 @@ func (c *HyperlaneClient) QuoteProcessUUSDC(ctx context.Context, domain string, return nil, fmt.Errorf("simulating process tx: %w", err) } - txFeeUUSDC, err := c.txPriceOracle.TxFeeUUSDC(ctx, unsentProcessTx, destinationChainConfig.GasTokenCoingeckoID) + txFeeUUSDC, err := c.txPriceOracle.TxFeeUUSDC(ctx, unsentProcessTx) if err != nil { return nil, fmt.Errorf("getting tx fee in uusdc from gas oracle: %w", err) } From 94bb59e4a42d8632a75a2aac2d0c6e2e06968e45 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Tue, 19 Nov 2024 16:47:44 -0500 Subject: [PATCH 07/20] get gas cost in uusdc when verifying a submitted tx --- txverifier/txverifier.go | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/txverifier/txverifier.go b/txverifier/txverifier.go index 94d6ac8..d46b60d 100644 --- a/txverifier/txverifier.go +++ b/txverifier/txverifier.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "math/big" "time" dbtypes "github.com/skip-mev/go-fast-solver/db" @@ -30,15 +31,21 @@ type Database interface { SetSubmittedTxStatus(ctx context.Context, arg db.SetSubmittedTxStatusParams) (db.SubmittedTx, error) } +type Oracle interface { + GasCostUUSDC(ctx context.Context, txFee *big.Int, chainID string) (*big.Int, error) +} + type TxVerifier struct { db Database clientManager *clientmanager.ClientManager + oracle Oracle } -func NewTxVerifier(ctx context.Context, db Database, clientManager *clientmanager.ClientManager) (*TxVerifier, error) { +func NewTxVerifier(ctx context.Context, db Database, clientManager *clientmanager.ClientManager, oracle Oracle) (*TxVerifier, error) { return &TxVerifier{ db: db, clientManager: clientManager, + oracle: oracle, }, nil } @@ -92,27 +99,41 @@ func (r *TxVerifier) VerifyTx(ctx context.Context, submittedTx db.SubmittedTx) e if err != nil { return fmt.Errorf("failed to get client: %w", err) } - _, failure, err := bridgeClient.GetTxResult(ctx, submittedTx.TxHash) + gasCost, failure, err := bridgeClient.GetTxResult(ctx, submittedTx.TxHash) if err != nil { return fmt.Errorf("failed to get tx result: %w", err) } else if failure != nil { lmt.Logger(ctx).Error("tx failed", zap.String("failure", failure.String())) + + cost, err := r.oracle.GasCostUUSDC(ctx, gasCost, submittedTx.ChainID) + if err != nil { + return fmt.Errorf("getting gas cost in uusdc for failed tx %s on chain %s: %w", submittedTx.TxHash, submittedTx.ChainID, err) + } + metrics.FromContext(ctx).IncTransactionVerified(false, submittedTx.ChainID) if _, err := r.db.SetSubmittedTxStatus(ctx, db.SetSubmittedTxStatusParams{ TxStatus: dbtypes.TxStatusFailed, TxHash: submittedTx.TxHash, ChainID: submittedTx.ChainID, TxStatusMessage: sql.NullString{String: failure.String(), Valid: true}, + TxCostUusdc: sql.NullString{String: cost.String(), Valid: true}, }); err != nil { return fmt.Errorf("failed to set tx status to failed: %w", err) } return fmt.Errorf("tx failed: %s", failure.String()) } else { metrics.FromContext(ctx).IncTransactionVerified(true, submittedTx.ChainID) + + cost, err := r.oracle.GasCostUUSDC(ctx, gasCost, submittedTx.ChainID) + if err != nil { + return fmt.Errorf("getting gas cost in uusdc for tx %s on chain %s: %w", submittedTx.TxHash, submittedTx.ChainID, err) + } + if _, err := r.db.SetSubmittedTxStatus(ctx, db.SetSubmittedTxStatusParams{ - TxStatus: dbtypes.TxStatusSuccess, - TxHash: submittedTx.TxHash, - ChainID: submittedTx.ChainID, + TxStatus: dbtypes.TxStatusSuccess, + TxHash: submittedTx.TxHash, + ChainID: submittedTx.ChainID, + TxCostUusdc: sql.NullString{String: cost.String(), Valid: true}, }); err != nil { return fmt.Errorf("failed to set tx status to success: %w", err) } From 8133aaafc0bb51186445057b4f8f7e21b309f36f Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Wed, 20 Nov 2024 11:12:35 -0500 Subject: [PATCH 08/20] add rebalance_transfer_id column to submitted txs table --- db/gen/db/models.go | 1 + db/gen/db/transactions.sql.go | 20 +++++++++++++------ ...ransfer_id_column_to_submitted_tx.down.sql | 1 + ..._transfer_id_column_to_submitted_tx.up.sql | 1 + db/queries/transactions.sql | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql create mode 100644 db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql diff --git a/db/gen/db/models.go b/db/gen/db/models.go index 001a74e..f4554c8 100644 --- a/db/gen/db/models.go +++ b/db/gen/db/models.go @@ -87,6 +87,7 @@ type SubmittedTx struct { TxStatus string TxStatusMessage sql.NullString TxCostUusdc sql.NullString + RebalanceTransferID sql.NullInt64 } type TransferMonitorMetadatum struct { diff --git a/db/gen/db/transactions.sql.go b/db/gen/db/transactions.sql.go index d60379a..cb87565 100644 --- a/db/gen/db/transactions.sql.go +++ b/db/gen/db/transactions.sql.go @@ -11,7 +11,7 @@ import ( ) const getSubmittedTxsByHyperlaneTransferId = `-- name: GetSubmittedTxsByHyperlaneTransferId :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE hyperlane_transfer_id = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc, rebalance_transfer_id FROM submitted_txs WHERE hyperlane_transfer_id = ? ` func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hyperlaneTransferID sql.NullInt64) ([]SubmittedTx, error) { @@ -37,6 +37,7 @@ func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hype &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ); err != nil { return nil, err } @@ -52,7 +53,7 @@ func (q *Queries) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hype } const getSubmittedTxsByOrderIdAndType = `-- name: GetSubmittedTxsByOrderIdAndType :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE order_id = ? AND tx_type = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc, rebalance_transfer_id FROM submitted_txs WHERE order_id = ? AND tx_type = ? ` type GetSubmittedTxsByOrderIdAndTypeParams struct { @@ -83,6 +84,7 @@ func (q *Queries) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSu &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ); err != nil { return nil, err } @@ -98,7 +100,7 @@ func (q *Queries) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSu } const getSubmittedTxsByOrderStatusAndType = `-- name: GetSubmittedTxsByOrderStatusAndType :many -SELECT submitted_txs.id, submitted_txs.created_at, submitted_txs.updated_at, submitted_txs.order_id, submitted_txs.order_settlement_id, submitted_txs.hyperlane_transfer_id, submitted_txs.chain_id, submitted_txs.tx_hash, submitted_txs.raw_tx, submitted_txs.tx_type, submitted_txs.tx_status, submitted_txs.tx_status_message, submitted_txs.tx_cost_uusdc FROM submitted_txs INNER JOIN orders on submitted_txs.order_id = orders.id WHERE orders.order_status = ? AND submitted_txs.tx_type = ? +SELECT submitted_txs.id, submitted_txs.created_at, submitted_txs.updated_at, submitted_txs.order_id, submitted_txs.order_settlement_id, submitted_txs.hyperlane_transfer_id, submitted_txs.chain_id, submitted_txs.tx_hash, submitted_txs.raw_tx, submitted_txs.tx_type, submitted_txs.tx_status, submitted_txs.tx_status_message, submitted_txs.tx_cost_uusdc, submitted_txs.rebalance_transfer_id FROM submitted_txs INNER JOIN orders on submitted_txs.order_id = orders.id WHERE orders.order_status = ? AND submitted_txs.tx_type = ? ` type GetSubmittedTxsByOrderStatusAndTypeParams struct { @@ -129,6 +131,7 @@ func (q *Queries) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg G &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ); err != nil { return nil, err } @@ -144,7 +147,7 @@ func (q *Queries) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg G } const getSubmittedTxsWithStatus = `-- name: GetSubmittedTxsWithStatus :many -SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc FROM submitted_txs WHERE tx_status = ? +SELECT id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc, rebalance_transfer_id FROM submitted_txs WHERE tx_status = ? ` func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string) ([]SubmittedTx, error) { @@ -170,6 +173,7 @@ func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ); err != nil { return nil, err } @@ -185,13 +189,14 @@ func (q *Queries) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string } const insertSubmittedTx = `-- name: InsertSubmittedTx :one -INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc +INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, rebalance_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc, rebalance_transfer_id ` type InsertSubmittedTxParams struct { OrderID sql.NullInt64 OrderSettlementID sql.NullInt64 HyperlaneTransferID sql.NullInt64 + RebalanceTransferID sql.NullInt64 ChainID string TxHash string RawTx string @@ -204,6 +209,7 @@ func (q *Queries) InsertSubmittedTx(ctx context.Context, arg InsertSubmittedTxPa arg.OrderID, arg.OrderSettlementID, arg.HyperlaneTransferID, + arg.RebalanceTransferID, arg.ChainID, arg.TxHash, arg.RawTx, @@ -225,6 +231,7 @@ func (q *Queries) InsertSubmittedTx(ctx context.Context, arg InsertSubmittedTxPa &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ) return i, err } @@ -232,7 +239,7 @@ func (q *Queries) InsertSubmittedTx(ctx context.Context, arg InsertSubmittedTxPa const setSubmittedTxStatus = `-- name: SetSubmittedTxStatus :one UPDATE submitted_txs SET tx_status = ?, tx_status_message = ?, tx_cost_uusdc = ?, updated_at = CURRENT_TIMESTAMP -WHERE tx_hash = ? AND chain_id = ? RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc +WHERE tx_hash = ? AND chain_id = ? RETURNING id, created_at, updated_at, order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status, tx_status_message, tx_cost_uusdc, rebalance_transfer_id ` type SetSubmittedTxStatusParams struct { @@ -266,6 +273,7 @@ func (q *Queries) SetSubmittedTxStatus(ctx context.Context, arg SetSubmittedTxSt &i.TxStatus, &i.TxStatusMessage, &i.TxCostUusdc, + &i.RebalanceTransferID, ) return i, err } diff --git a/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql b/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql new file mode 100644 index 0000000..befab5d --- /dev/null +++ b/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql @@ -0,0 +1 @@ +ALTER TABLE submitted_txs DROP COLUMN rebalance_transfer_id; diff --git a/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql b/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql new file mode 100644 index 0000000..2a90318 --- /dev/null +++ b/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql @@ -0,0 +1 @@ +ALTER TABLE submitted_txs ADD rebalance_transfer_id INT; diff --git a/db/queries/transactions.sql b/db/queries/transactions.sql index 560cccc..b946825 100644 --- a/db/queries/transactions.sql +++ b/db/queries/transactions.sql @@ -1,5 +1,5 @@ -- name: InsertSubmittedTx :one -INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; +INSERT INTO submitted_txs (order_id, order_settlement_id, hyperlane_transfer_id, rebalance_transfer_id, chain_id, tx_hash, raw_tx, tx_type, tx_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; -- name: GetSubmittedTxsByOrderIdAndType :many SELECT * FROM submitted_txs WHERE order_id = ? AND tx_type = ?; From 1920828fd41c53e9e46ef309d7f9b2341d0c4c16 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Wed, 20 Nov 2024 15:04:52 -0500 Subject: [PATCH 09/20] insert fund rebalancer txs into submitted txs table --- db/types.go | 2 + fundrebalancer/fundrebalancer.go | 83 +++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/db/types.go b/db/types.go index 07b99f3..e0ac2f3 100644 --- a/db/types.go +++ b/db/types.go @@ -21,6 +21,8 @@ const ( TxTypeSettlement string = "SETTLEMENT" TxTypeHyperlaneMessageDelivery string = "HYPERLANE_MESSAGE_DELIVERY" TxTypeInitiateTimeout string = "INITIATE_TIMEOUT" + TxTypeERC20Approval string = "ERC20_APPROVAL" + TxTypeFundRebalnance string = "FUND_REBALANCE" RebalanceTransactionStatusPending string = "PENDING" RebalanceTransactionStatusSuccess string = "SUCCESS" diff --git a/fundrebalancer/fundrebalancer.go b/fundrebalancer/fundrebalancer.go index c73ccc9..30925fd 100644 --- a/fundrebalancer/fundrebalancer.go +++ b/fundrebalancer/fundrebalancer.go @@ -1,6 +1,7 @@ package fundrebalancer import ( + "database/sql" "encoding/hex" "encoding/json" "fmt" @@ -37,6 +38,7 @@ type Database interface { InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) GetAllPendingRebalanceTransfers(ctx context.Context) ([]db.GetAllPendingRebalanceTransfersRow, error) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error + InsertSubmittedTx(ctx context.Context, arg db.InsertSubmittedTxParams) (db.SubmittedTx, error) } type FundRebalancer struct { @@ -283,18 +285,55 @@ func (r *FundRebalancer) MoveFundsToChain( } } - if err = r.ERC20Approval(ctx, txn); err != nil { + approvalHash, err := r.ERC20Approval(ctx, txn) + if err != nil { return nil, nil, fmt.Errorf("approving usdc erc20 spend on chain %s for %suusdc: %w", rebalanceFromChainID, usdcToRebalance.String(), err) } - txHash, err := r.SignAndSubmitTxn(ctx, txn) + rebalanceHash, err := r.SignAndSubmitTxn(ctx, txn) if err != nil { return nil, nil, fmt.Errorf("signing and submitting transaction: %w", err) } metrics.FromContext(ctx).IncFundsRebalanceTransferStatusChange(rebalanceFromChainID, rebalanceToChain, dbtypes.RebalanceTransactionStatusPending) + // add rebalance transfer to the db + rebalanceTransfer := db.InsertRebalanceTransferParams{ + TxHash: string(rebalanceHash), + SourceChainID: txn.sourceChainID, + DestinationChainID: txn.destinationChainID, + Amount: txn.amount.String(), + } + rebalanceID, err := r.database.InsertRebalanceTransfer(ctx, rebalanceTransfer) + if err != nil { + return nil, nil, fmt.Errorf("inserting rebalance transfer with hash %s into db: %w", rebalanceHash, err) + } + + // add erc20 approval tx to submitted txs table + approveTx := db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: rebalanceID, Valid: true}, + ChainID: txn.sourceChainID, + TxHash: string(approvalHash), + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, + } + if _, err = r.database.InsertSubmittedTx(ctx, approveTx); err != nil { + return nil, nil, fmt.Errorf("inserting submitted tx for erc20 approval with hash %s into db: %w", approvalHash, err) + } + + // add rebalance tx to submitted txs table + rebalanceTx := db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: rebalanceID, Valid: true}, + ChainID: txn.sourceChainID, + TxHash: string(rebalanceHash), + TxType: dbtypes.TxTypeFundRebalnance, + TxStatus: dbtypes.TxStatusPending, + } + if _, err = r.database.InsertSubmittedTx(ctx, rebalanceTx); err != nil { + return nil, nil, fmt.Errorf("inserting submitted tx for rebalance transfer with hash %s into db: %w", rebalanceHash, err) + } + totalUSDCcMoved = new(big.Int).Add(totalUSDCcMoved, usdcToRebalance) - hashes = append(hashes, txHash) + hashes = append(hashes, rebalanceHash) // if there is no more usdc needed, we are done rebalancing remainingUSDCNeeded = new(big.Int).Sub(remainingUSDCNeeded, usdcToRebalance) @@ -552,16 +591,6 @@ func (r *FundRebalancer) SignAndSubmitTxn( zap.String("txnHash", txHash), ) - args := db.InsertRebalanceTransferParams{ - TxHash: txHash, - SourceChainID: txn.sourceChainID, - DestinationChainID: txn.destinationChainID, - Amount: txn.amount.String(), - } - if _, err := r.database.InsertRebalanceTransfer(ctx, args); err != nil { - return "", fmt.Errorf("inserting rebalance txHash with hash %s into db: %w", txHash, err) - } - return skipgo.TxHash(txHash), nil case txn.tx.CosmosTx != nil: return "", fmt.Errorf("cosmos txns not supported yet") @@ -570,59 +599,59 @@ func (r *FundRebalancer) SignAndSubmitTxn( } } -func (r *FundRebalancer) ERC20Approval(ctx context.Context, txn SkipGoTxnWithMetadata) error { +func (r *FundRebalancer) ERC20Approval(ctx context.Context, txn SkipGoTxnWithMetadata) (string, error) { if txn.tx.EVMTx == nil { // if this isnt an evm tx, no erc20 approvals are required - return nil + return "", nil } evmTx := txn.tx.EVMTx if len(evmTx.RequiredERC20Approvals) == 0 { // if no approvals are required, return with no error - return nil + return "", nil } if len(evmTx.RequiredERC20Approvals) > 1 { // only support single approval - return fmt.Errorf("expected 1 required erc20 approval but got %d", len(evmTx.RequiredERC20Approvals)) + return "", fmt.Errorf("expected 1 required erc20 approval but got %d", len(evmTx.RequiredERC20Approvals)) } approval := evmTx.RequiredERC20Approvals[0] chainConfig, err := config.GetConfigReader(ctx).GetChainConfig(evmTx.ChainID) if err != nil { - return fmt.Errorf("getting config for chain %s: %w", evmTx.ChainID, err) + return "", fmt.Errorf("getting config for chain %s: %w", evmTx.ChainID, err) } usdcDenom, err := config.GetConfigReader(ctx).GetUSDCDenom(evmTx.ChainID) if err != nil { - return fmt.Errorf("fetching usdc denom on chain %s: %w", evmTx.ChainID, err) + return "", fmt.Errorf("fetching usdc denom on chain %s: %w", evmTx.ChainID, err) } // sanity check on the address being returned to be what the solver expects if !strings.EqualFold(approval.TokenContract, usdcDenom) { - return fmt.Errorf("expected required approval for usdc token contract %s, but got %s", usdcDenom, approval.TokenContract) + return "", fmt.Errorf("expected required approval for usdc token contract %s, but got %s", usdcDenom, approval.TokenContract) } signer, err := signing.NewSigner(ctx, evmTx.ChainID, r.chainIDToPrivateKey) if err != nil { - return fmt.Errorf("creating signer for chain %s: %w", evmTx.ChainID, err) + return "", fmt.Errorf("creating signer for chain %s: %w", evmTx.ChainID, err) } spender := common.HexToAddress(approval.Spender) amount, ok := new(big.Int).SetString(approval.Amount, 10) if !ok { - return fmt.Errorf("error converting erc20 approval amount %s on chain %s to *big.Int", approval.Amount, evmTx.ChainID) + return "", fmt.Errorf("error converting erc20 approval amount %s on chain %s to *big.Int", approval.Amount, evmTx.ChainID) } abi, err := usdc.UsdcMetaData.GetAbi() if err != nil { - return fmt.Errorf("getting usdc contract abi: %w", err) + return "", fmt.Errorf("getting usdc contract abi: %w", err) } input, err := abi.Pack("approve", spender, amount) if err != nil { - return fmt.Errorf("packing input to erc20 approval tx: %w", err) + return "", fmt.Errorf("packing input to erc20 approval tx: %w", err) } - _, err = r.evmTxExecutor.ExecuteTx( + hash, err := r.evmTxExecutor.ExecuteTx( ctx, evmTx.ChainID, chainConfig.SolverAddress, @@ -632,10 +661,10 @@ func (r *FundRebalancer) ERC20Approval(ctx context.Context, txn SkipGoTxnWithMet signer, ) if err != nil { - return fmt.Errorf("executing erc20 approve for %s at contract %s for spender %s on %s: %w", amount.String(), approval.TokenContract, approval.Spender, evmTx.ChainID, err) + return "", fmt.Errorf("executing erc20 approve for %s at contract %s for spender %s on %s: %w", amount.String(), approval.TokenContract, approval.Spender, evmTx.ChainID, err) } - return nil + return hash, nil } func (r *FundRebalancer) estimateTotalGas(txns []SkipGoTxnWithMetadata) (uint64, error) { From 51c66f4604edc8f90d1f31860716a979af96ef50 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Wed, 20 Nov 2024 15:40:09 -0500 Subject: [PATCH 10/20] erc20 approval before gas cost estimation --- fundrebalancer/fundrebalancer.go | 125 ++++++++++++++++--------------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/fundrebalancer/fundrebalancer.go b/fundrebalancer/fundrebalancer.go index 30925fd..aa56356 100644 --- a/fundrebalancer/fundrebalancer.go +++ b/fundrebalancer/fundrebalancer.go @@ -263,13 +263,23 @@ func (r *FundRebalancer) MoveFundsToChain( } txn := txns[0] + approvalHash, err := r.ERC20Approval(ctx, txn) + if err != nil { + return nil, nil, fmt.Errorf("approving usdc erc20 spend on chain %s for %suusdc: %w", rebalanceFromChainID, usdcToRebalance.String(), err) + } + + txnWithMetadata, err := r.TxnWithMetadata(ctx, rebalanceFromChainID, rebalanceFromChainID, usdcToRebalance, txn) + if err != nil { + return nil, nil, fmt.Errorf("getting transaction metadata: %w", err) + } + chainConfig, err := config.GetConfigReader(ctx).GetChainConfig(rebalanceFromChainID) if err != nil { return nil, nil, fmt.Errorf("getting chain config for gas threshold check: %w", err) } if chainConfig.MaxRebalancingGasThreshold != 0 { - gasAcceptable, totalRebalancingGas, err := r.isGasAcceptable(txns, chainConfig.MaxRebalancingGasThreshold) + gasAcceptable, totalRebalancingGas, err := r.isGasAcceptable(txn, chainConfig.MaxRebalancingGasThreshold) if err != nil { return nil, nil, fmt.Errorf("checking if gas amount is acceptable: %w", err) } @@ -285,11 +295,6 @@ func (r *FundRebalancer) MoveFundsToChain( } } - approvalHash, err := r.ERC20Approval(ctx, txn) - if err != nil { - return nil, nil, fmt.Errorf("approving usdc erc20 spend on chain %s for %suusdc: %w", rebalanceFromChainID, usdcToRebalance.String(), err) - } - rebalanceHash, err := r.SignAndSubmitTxn(ctx, txn) if err != nil { return nil, nil, fmt.Errorf("signing and submitting transaction: %w", err) @@ -444,7 +449,7 @@ func (r *FundRebalancer) GetRebalanceTxns( amount *big.Int, sourceChainID string, destChainID string, -) ([]SkipGoTxnWithMetadata, error) { +) ([]skipgo.Tx, error) { rebalanceFromDenom, err := config.GetConfigReader(ctx).GetUSDCDenom(sourceChainID) if err != nil { return nil, fmt.Errorf("getting usdc denom for chain %s: %w", sourceChainID, err) @@ -513,43 +518,55 @@ func (r *FundRebalancer) GetRebalanceTxns( return nil, fmt.Errorf("getting rebalancing txn operations from Skip Go: %w", err) } - txnsWithMetadata := make([]SkipGoTxnWithMetadata, 0, len(txns)) - for _, txn := range txns { - var gasEstimate uint64 - if txn.EVMTx != nil { - client, err := r.evmClientManager.GetClient(ctx, txn.EVMTx.ChainID) - if err != nil { - return nil, fmt.Errorf("getting evm client for chain %s: %w", txn.EVMTx.ChainID, err) - } + return txns, nil +} - decodedData, err := hex.DecodeString(txn.EVMTx.Data) - if err != nil { - return nil, fmt.Errorf("hex decoding evm call data: %w", err) - } +func (r *FundRebalancer) TxnWithMetadata( + ctx context.Context, + sourceChainID string, + destinationChainID string, + amount *big.Int, + txn skipgo.Tx, +) (SkipGoTxnWithMetadata, error) { + sourceChainConfig, err := config.GetConfigReader(ctx).GetChainConfig(sourceChainID) + if err != nil { + return SkipGoTxnWithMetadata{}, fmt.Errorf("getting source chain config for chain %s: %w", sourceChainID, err) + } - txBuilder := evm.NewTxBuilder(client) - estimate, err := txBuilder.EstimateGasForTx( - ctx, - sourceChainConfig.SolverAddress, - txn.EVMTx.To, - txn.EVMTx.Value, - decodedData, - ) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } - gasEstimate = estimate - } - txnsWithMetadata = append(txnsWithMetadata, SkipGoTxnWithMetadata{ - tx: txn, - sourceChainID: sourceChainID, - destinationChainID: destChainID, - amount: amount, - gasEstimate: gasEstimate, - }) + var gasEstimate uint64 + if txn.EVMTx == nil { + return SkipGoTxnWithMetadata{}, fmt.Errorf("evm tx cannot be nil") + } + client, err := r.evmClientManager.GetClient(ctx, txn.EVMTx.ChainID) + if err != nil { + return SkipGoTxnWithMetadata{}, fmt.Errorf("getting evm client for chain %s: %w", txn.EVMTx.ChainID, err) + } + + decodedData, err := hex.DecodeString(txn.EVMTx.Data) + if err != nil { + return SkipGoTxnWithMetadata{}, fmt.Errorf("hex decoding evm call data: %w", err) } - return txnsWithMetadata, nil + txBuilder := evm.NewTxBuilder(client) + estimate, err := txBuilder.EstimateGasForTx( + ctx, + sourceChainConfig.SolverAddress, + txn.EVMTx.To, + txn.EVMTx.Value, + decodedData, + ) + if err != nil { + return SkipGoTxnWithMetadata{}, fmt.Errorf("estimating gas: %w", err) + } + gasEstimate = estimate + + return SkipGoTxnWithMetadata{ + tx: txn, + sourceChainID: sourceChainID, + destinationChainID: destinationChainID, + amount: amount, + gasEstimate: gasEstimate, + }, nil } // SignAndSubmitTxn signs and submits txs to chain @@ -599,12 +616,13 @@ func (r *FundRebalancer) SignAndSubmitTxn( } } -func (r *FundRebalancer) ERC20Approval(ctx context.Context, txn SkipGoTxnWithMetadata) (string, error) { - if txn.tx.EVMTx == nil { +func (r *FundRebalancer) ERC20Approval(ctx context.Context, tx skipgo.Tx) (string, error) { + if tx.EVMTx == nil { // if this isnt an evm tx, no erc20 approvals are required return "", nil } - evmTx := txn.tx.EVMTx + + evmTx := tx.EVMTx if len(evmTx.RequiredERC20Approvals) == 0 { // if no approvals are required, return with no error return "", nil @@ -667,24 +685,11 @@ func (r *FundRebalancer) ERC20Approval(ctx context.Context, txn SkipGoTxnWithMet return hash, nil } -func (r *FundRebalancer) estimateTotalGas(txns []SkipGoTxnWithMetadata) (uint64, error) { - var totalGas uint64 - for _, txn := range txns { - totalGas += txn.gasEstimate - } - return totalGas, nil -} - -func (r *FundRebalancer) isGasAcceptable(txns []SkipGoTxnWithMetadata, maxRebalancingGasThreshold uint64) (bool, uint64, error) { +func (r *FundRebalancer) isGasAcceptable(txn SkipGoTxnWithMetadata, maxRebalancingGasThreshold uint64) (bool, uint64, error) { // Check if total gas needed exceeds threshold to rebalance funds from this chain - totalRebalancingGas, err := r.estimateTotalGas(txns) - if err != nil { - return false, 0, fmt.Errorf("estimating total gas for transactions: %w", err) - } - - if totalRebalancingGas > maxRebalancingGasThreshold { - return false, totalRebalancingGas, nil + if txn.gasEstimate > maxRebalancingGasThreshold { + return false, txn.gasEstimate, nil } - return true, totalRebalancingGas, nil + return true, txn.gasEstimate, nil } From 6cd7b2343331aef7765a8438c61d469515ce6d16 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Mon, 2 Dec 2024 16:15:02 -0500 Subject: [PATCH 11/20] move evm tx price oracle to generic oracle package to not be evm specific --- .mockery.yml | 4 +- mocks/fundrebalancer/fake_database.go | 46 ++++ mocks/fundrebalancer/mock_database.go | 222 +++++++++++++++++++- mocks/shared/evmrpc/mock_oracle.go | 65 ------ mocks/shared/oracle/mock_tx_price_oracle.go | 158 ++++++++++++++ shared/evmrpc/tx_price_oracle.go | 94 --------- shared/oracle/tx_price_oracle.go | 10 + 7 files changed, 438 insertions(+), 161 deletions(-) delete mode 100644 mocks/shared/evmrpc/mock_oracle.go create mode 100644 mocks/shared/oracle/mock_tx_price_oracle.go delete mode 100644 shared/evmrpc/tx_price_oracle.go diff --git a/.mockery.yml b/.mockery.yml index 5ad72d7..c7f01c6 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -6,7 +6,9 @@ packages: interfaces: EVMRPCClientManager: EVMChainRPC: - IOracle: + github.com/skip-mev/go-fast-solver/shared/oracle: + interfaces: + TxPriceOracle: github.com/skip-mev/go-fast-solver/shared/http: interfaces: Client: diff --git a/mocks/fundrebalancer/fake_database.go b/mocks/fundrebalancer/fake_database.go index 7a1c6ba..9335d04 100644 --- a/mocks/fundrebalancer/fake_database.go +++ b/mocks/fundrebalancer/fake_database.go @@ -50,6 +50,38 @@ func (fdb *FakeDatabase) GetPendingRebalanceTransfersToChain(ctx context.Context return pendingTransfers, nil } +func (fdb *FakeDatabase) InsertSubmittedTx(ctx context.Context, arg db.InsertSubmittedTxParams) (db.SubmittedTx, error) { + return db.SubmittedTx{}, nil +} + +func (fdb *FakeDatabase) InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) { + return fdb.InsertRebalanceTransfer(ctx, db.InsertRebalanceTransferParams{ + TxHash: "", + SourceChainID: arg.SourceChainID, + DestinationChainID: arg.DestinationChainID, + Amount: "0", + }) +} + +func (fdb *FakeDatabase) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) { + fdb.dbLock.RLock() + defer fdb.dbLock.RUnlock() + + var pendingTransfers []db.GetPendingRebalanceTransfersBetweenChainsRow + for _, transfer := range fdb.db { + if transfer.Status == "PENDING" && transfer.DestinationChainID == arg.DestinationChainID && transfer.SourceChainID == arg.SourceChainID { + pendingTransfers = append(pendingTransfers, db.GetPendingRebalanceTransfersBetweenChainsRow{ + ID: transfer.ID, + TxHash: transfer.TxHash, + SourceChainID: transfer.SourceChainID, + DestinationChainID: transfer.DestinationChainID, + Amount: transfer.Amount, + }) + } + } + return pendingTransfers, nil +} + func (fdb *FakeDatabase) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) { fdb.dbLock.Lock() defer fdb.dbLock.Unlock() @@ -92,6 +124,20 @@ func (fdb *FakeDatabase) GetAllPendingRebalanceTransfers(ctx context.Context) ([ return pendingTransfers, nil } +func (fdb *FakeDatabase) UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error { + fdb.dbLock.Lock() + defer fdb.dbLock.Unlock() + + for _, transfer := range fdb.db { + if transfer.ID == arg.ID { + transfer.TxHash = arg.TxHash + transfer.Amount = arg.Amount + } + } + + return nil +} + func (fdb *FakeDatabase) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error { fdb.dbLock.Lock() defer fdb.dbLock.Unlock() diff --git a/mocks/fundrebalancer/mock_database.go b/mocks/fundrebalancer/mock_database.go index 8dd38dc..cb8546b 100644 --- a/mocks/fundrebalancer/mock_database.go +++ b/mocks/fundrebalancer/mock_database.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.47.0. DO NOT EDIT. +// Code generated by mockery v2.46.2. DO NOT EDIT. package fundrebalancer @@ -80,6 +80,65 @@ func (_c *MockDatabase_GetAllPendingRebalanceTransfers_Call) RunAndReturn(run fu return _c } +// GetPendingRebalanceTransfersBetweenChains provides a mock function with given fields: ctx, arg +func (_m *MockDatabase) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for GetPendingRebalanceTransfersBetweenChains") + } + + var r0 []db.GetPendingRebalanceTransfersBetweenChainsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) []db.GetPendingRebalanceTransfersBetweenChainsRow); ok { + r0 = rf(ctx, arg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.GetPendingRebalanceTransfersBetweenChainsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPendingRebalanceTransfersBetweenChains' +type MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call struct { + *mock.Call +} + +// GetPendingRebalanceTransfersBetweenChains is a helper method to define mock.On call +// - ctx context.Context +// - arg db.GetPendingRebalanceTransfersBetweenChainsParams +func (_e *MockDatabase_Expecter) GetPendingRebalanceTransfersBetweenChains(ctx interface{}, arg interface{}) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { + return &MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call{Call: _e.mock.On("GetPendingRebalanceTransfersBetweenChains", ctx, arg)} +} + +func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) Run(run func(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams)) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(db.GetPendingRebalanceTransfersBetweenChainsParams)) + }) + return _c +} + +func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) Return(_a0 []db.GetPendingRebalanceTransfersBetweenChainsRow, _a1 error) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) RunAndReturn(run func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error)) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { + _c.Call.Return(run) + return _c +} + // GetPendingRebalanceTransfersToChain provides a mock function with given fields: ctx, destinationChainID func (_m *MockDatabase) GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]db.GetPendingRebalanceTransfersToChainRow, error) { ret := _m.Called(ctx, destinationChainID) @@ -139,6 +198,63 @@ func (_c *MockDatabase_GetPendingRebalanceTransfersToChain_Call) RunAndReturn(ru return _c } +// InitializeRebalanceTransfer provides a mock function with given fields: ctx, arg +func (_m *MockDatabase) InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for InitializeRebalanceTransfer") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, db.InitializeRebalanceTransferParams) (int64, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, db.InitializeRebalanceTransferParams) int64); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, db.InitializeRebalanceTransferParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_InitializeRebalanceTransfer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitializeRebalanceTransfer' +type MockDatabase_InitializeRebalanceTransfer_Call struct { + *mock.Call +} + +// InitializeRebalanceTransfer is a helper method to define mock.On call +// - ctx context.Context +// - arg db.InitializeRebalanceTransferParams +func (_e *MockDatabase_Expecter) InitializeRebalanceTransfer(ctx interface{}, arg interface{}) *MockDatabase_InitializeRebalanceTransfer_Call { + return &MockDatabase_InitializeRebalanceTransfer_Call{Call: _e.mock.On("InitializeRebalanceTransfer", ctx, arg)} +} + +func (_c *MockDatabase_InitializeRebalanceTransfer_Call) Run(run func(ctx context.Context, arg db.InitializeRebalanceTransferParams)) *MockDatabase_InitializeRebalanceTransfer_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(db.InitializeRebalanceTransferParams)) + }) + return _c +} + +func (_c *MockDatabase_InitializeRebalanceTransfer_Call) Return(_a0 int64, _a1 error) *MockDatabase_InitializeRebalanceTransfer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_InitializeRebalanceTransfer_Call) RunAndReturn(run func(context.Context, db.InitializeRebalanceTransferParams) (int64, error)) *MockDatabase_InitializeRebalanceTransfer_Call { + _c.Call.Return(run) + return _c +} + // InsertRebalanceTransfer provides a mock function with given fields: ctx, arg func (_m *MockDatabase) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) { ret := _m.Called(ctx, arg) @@ -196,6 +312,110 @@ func (_c *MockDatabase_InsertRebalanceTransfer_Call) RunAndReturn(run func(conte return _c } +// InsertSubmittedTx provides a mock function with given fields: ctx, arg +func (_m *MockDatabase) InsertSubmittedTx(ctx context.Context, arg db.InsertSubmittedTxParams) (db.SubmittedTx, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for InsertSubmittedTx") + } + + var r0 db.SubmittedTx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, db.InsertSubmittedTxParams) (db.SubmittedTx, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, db.InsertSubmittedTxParams) db.SubmittedTx); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(db.SubmittedTx) + } + + if rf, ok := ret.Get(1).(func(context.Context, db.InsertSubmittedTxParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_InsertSubmittedTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertSubmittedTx' +type MockDatabase_InsertSubmittedTx_Call struct { + *mock.Call +} + +// InsertSubmittedTx is a helper method to define mock.On call +// - ctx context.Context +// - arg db.InsertSubmittedTxParams +func (_e *MockDatabase_Expecter) InsertSubmittedTx(ctx interface{}, arg interface{}) *MockDatabase_InsertSubmittedTx_Call { + return &MockDatabase_InsertSubmittedTx_Call{Call: _e.mock.On("InsertSubmittedTx", ctx, arg)} +} + +func (_c *MockDatabase_InsertSubmittedTx_Call) Run(run func(ctx context.Context, arg db.InsertSubmittedTxParams)) *MockDatabase_InsertSubmittedTx_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(db.InsertSubmittedTxParams)) + }) + return _c +} + +func (_c *MockDatabase_InsertSubmittedTx_Call) Return(_a0 db.SubmittedTx, _a1 error) *MockDatabase_InsertSubmittedTx_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_InsertSubmittedTx_Call) RunAndReturn(run func(context.Context, db.InsertSubmittedTxParams) (db.SubmittedTx, error)) *MockDatabase_InsertSubmittedTx_Call { + _c.Call.Return(run) + return _c +} + +// UpdateTransfer provides a mock function with given fields: ctx, arg +func (_m *MockDatabase) UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for UpdateTransfer") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, db.UpdateTransferParams) error); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDatabase_UpdateTransfer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTransfer' +type MockDatabase_UpdateTransfer_Call struct { + *mock.Call +} + +// UpdateTransfer is a helper method to define mock.On call +// - ctx context.Context +// - arg db.UpdateTransferParams +func (_e *MockDatabase_Expecter) UpdateTransfer(ctx interface{}, arg interface{}) *MockDatabase_UpdateTransfer_Call { + return &MockDatabase_UpdateTransfer_Call{Call: _e.mock.On("UpdateTransfer", ctx, arg)} +} + +func (_c *MockDatabase_UpdateTransfer_Call) Run(run func(ctx context.Context, arg db.UpdateTransferParams)) *MockDatabase_UpdateTransfer_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(db.UpdateTransferParams)) + }) + return _c +} + +func (_c *MockDatabase_UpdateTransfer_Call) Return(_a0 error) *MockDatabase_UpdateTransfer_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDatabase_UpdateTransfer_Call) RunAndReturn(run func(context.Context, db.UpdateTransferParams) error) *MockDatabase_UpdateTransfer_Call { + _c.Call.Return(run) + return _c +} + // UpdateTransferStatus provides a mock function with given fields: ctx, arg func (_m *MockDatabase) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error { ret := _m.Called(ctx, arg) diff --git a/mocks/shared/evmrpc/mock_oracle.go b/mocks/shared/evmrpc/mock_oracle.go deleted file mode 100644 index 8fcbc36..0000000 --- a/mocks/shared/evmrpc/mock_oracle.go +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated by mockery v2.47.0. DO NOT EDIT. - -package evmrpc - -import ( - context "context" - big "math/big" - - types "github.com/ethereum/go-ethereum/core/types" - mock "github.com/stretchr/testify/mock" -) - -// MockOracle is an autogenerated mock type for the Oracle type -type MockOracle struct { - mock.Mock -} - -type MockOracle_Expecter struct { - mock *mock.Mock -} - -func (_m *MockOracle) EXPECT() *MockOracle_Expecter { - return &MockOracle_Expecter{mock: &_m.Mock} -} - -// TxFeeUUSDC provides a mock function with given fields: ctx, tx, chainID -func (_m *MockOracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction, chainID string) (*big.Int, error) { - ret := _m.Called(ctx, tx, chainID) - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, string) (*big.Int, error)); ok { - return rf(ctx, tx, chainID) - } - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, string) *big.Int); ok { - r0 = rf(ctx, tx, chainID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *types.Transaction, string) error); ok { - r1 = rf(ctx, tx, chainID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewMockEVMTxExecutor creates a new instance of MockEVMTxExecutor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockOracle(t interface { - mock.TestingT - Cleanup(func()) -}) *MockOracle { - mock := &MockOracle{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - diff --git a/mocks/shared/oracle/mock_tx_price_oracle.go b/mocks/shared/oracle/mock_tx_price_oracle.go new file mode 100644 index 0000000..ac4aa62 --- /dev/null +++ b/mocks/shared/oracle/mock_tx_price_oracle.go @@ -0,0 +1,158 @@ +// Code generated by mockery v2.46.2. DO NOT EDIT. + +package oracle + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" + + types "github.com/ethereum/go-ethereum/core/types" +) + +// MockTxPriceOracle is an autogenerated mock type for the TxPriceOracle type +type MockTxPriceOracle struct { + mock.Mock +} + +type MockTxPriceOracle_Expecter struct { + mock *mock.Mock +} + +func (_m *MockTxPriceOracle) EXPECT() *MockTxPriceOracle_Expecter { + return &MockTxPriceOracle_Expecter{mock: &_m.Mock} +} + +// GasCostUUSDC provides a mock function with given fields: ctx, txFee, chainID +func (_m *MockTxPriceOracle) GasCostUUSDC(ctx context.Context, txFee *big.Int, chainID string) (*big.Int, error) { + ret := _m.Called(ctx, txFee, chainID) + + if len(ret) == 0 { + panic("no return value specified for GasCostUUSDC") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, string) (*big.Int, error)); ok { + return rf(ctx, txFee, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, string) *big.Int); ok { + r0 = rf(ctx, txFee, chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int, string) error); ok { + r1 = rf(ctx, txFee, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTxPriceOracle_GasCostUUSDC_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GasCostUUSDC' +type MockTxPriceOracle_GasCostUUSDC_Call struct { + *mock.Call +} + +// GasCostUUSDC is a helper method to define mock.On call +// - ctx context.Context +// - txFee *big.Int +// - chainID string +func (_e *MockTxPriceOracle_Expecter) GasCostUUSDC(ctx interface{}, txFee interface{}, chainID interface{}) *MockTxPriceOracle_GasCostUUSDC_Call { + return &MockTxPriceOracle_GasCostUUSDC_Call{Call: _e.mock.On("GasCostUUSDC", ctx, txFee, chainID)} +} + +func (_c *MockTxPriceOracle_GasCostUUSDC_Call) Run(run func(ctx context.Context, txFee *big.Int, chainID string)) *MockTxPriceOracle_GasCostUUSDC_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int), args[2].(string)) + }) + return _c +} + +func (_c *MockTxPriceOracle_GasCostUUSDC_Call) Return(_a0 *big.Int, _a1 error) *MockTxPriceOracle_GasCostUUSDC_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTxPriceOracle_GasCostUUSDC_Call) RunAndReturn(run func(context.Context, *big.Int, string) (*big.Int, error)) *MockTxPriceOracle_GasCostUUSDC_Call { + _c.Call.Return(run) + return _c +} + +// TxFeeUUSDC provides a mock function with given fields: ctx, tx +func (_m *MockTxPriceOracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction) (*big.Int, error) { + ret := _m.Called(ctx, tx) + + if len(ret) == 0 { + panic("no return value specified for TxFeeUUSDC") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) (*big.Int, error)); ok { + return rf(ctx, tx) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) *big.Int); ok { + r0 = rf(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Transaction) error); ok { + r1 = rf(ctx, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTxPriceOracle_TxFeeUUSDC_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TxFeeUUSDC' +type MockTxPriceOracle_TxFeeUUSDC_Call struct { + *mock.Call +} + +// TxFeeUUSDC is a helper method to define mock.On call +// - ctx context.Context +// - tx *types.Transaction +func (_e *MockTxPriceOracle_Expecter) TxFeeUUSDC(ctx interface{}, tx interface{}) *MockTxPriceOracle_TxFeeUUSDC_Call { + return &MockTxPriceOracle_TxFeeUUSDC_Call{Call: _e.mock.On("TxFeeUUSDC", ctx, tx)} +} + +func (_c *MockTxPriceOracle_TxFeeUUSDC_Call) Run(run func(ctx context.Context, tx *types.Transaction)) *MockTxPriceOracle_TxFeeUUSDC_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Transaction)) + }) + return _c +} + +func (_c *MockTxPriceOracle_TxFeeUUSDC_Call) Return(_a0 *big.Int, _a1 error) *MockTxPriceOracle_TxFeeUUSDC_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTxPriceOracle_TxFeeUUSDC_Call) RunAndReturn(run func(context.Context, *types.Transaction) (*big.Int, error)) *MockTxPriceOracle_TxFeeUUSDC_Call { + _c.Call.Return(run) + return _c +} + +// NewMockTxPriceOracle creates a new instance of MockTxPriceOracle. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockTxPriceOracle(t interface { + mock.TestingT + Cleanup(func()) +}) *MockTxPriceOracle { + mock := &MockTxPriceOracle{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/shared/evmrpc/tx_price_oracle.go b/shared/evmrpc/tx_price_oracle.go deleted file mode 100644 index d9a908c..0000000 --- a/shared/evmrpc/tx_price_oracle.go +++ /dev/null @@ -1,94 +0,0 @@ -package evmrpc - -import ( - "context" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/core/types" - "github.com/skip-mev/go-fast-solver/shared/clients/coingecko" -) - -const ( - coingeckoUSDCurrency = "usd" -) - -var _ IOracle = (*Oracle)(nil) - -type IOracle interface { - TxFeeUUSDC(ctx context.Context, tx *types.Transaction, gasTokenCoingeckoID string) (*big.Int, error) -} - -// Oracle is a evm uusdc tx execution price oracle that determines the price of -// executing a tx on chain in uusdc. -type Oracle struct { - coingecko coingecko.PriceClient -} - -// NewOracle creates a new evm uusdc tx execution price oracle. -func NewOracle(coingecko coingecko.PriceClient) *Oracle { - return &Oracle{coingecko: coingecko} -} - -// TxFeeUUSDC estimates what the cost in uusdc would be to execute a tx. The -// tx's gas fee cap and gas limit must be set. -func (o *Oracle) TxFeeUUSDC(ctx context.Context, tx *types.Transaction, gasTokenCoingeckoID string) (*big.Int, error) { - if tx.Type() != types.DynamicFeeTxType { - return nil, fmt.Errorf("tx type must be dynamic fee tx, got %d", tx.Type()) - } - - // for a dry ran tx, GasFeeCap() will be the suggested gas tip cap + base - // fee of current chain head - estimatedPricePerGas := tx.GasFeeCap() - if estimatedPricePerGas == nil { - return nil, fmt.Errorf("tx's gas fee cap must be set") - } - - // for a dry ran tx, Gas() will be the result of calling eth_estimateGas - estimatedGasUsed := tx.Gas() - return o.gasCostUUSDC(ctx, estimatedPricePerGas, big.NewInt(int64(estimatedGasUsed)), gasTokenCoingeckoID) -} - -// gasCostUUSDC converts an amount of gas and the price per gas in gwei to -// uusdc based on the current CoinGecko price of ethereum in usd. -func (o *Oracle) gasCostUUSDC(ctx context.Context, pricePerGasWei *big.Int, gasUsed *big.Int, gasTokenCoingeckoID string) (*big.Int, error) { - // Calculate transaction fee in Wei - txFeeWei := new(big.Int).Mul(gasUsed, pricePerGasWei) - - // Get the ETH price in USD cents from CoinGecko - ethPriceUSD, err := o.coingecko.GetSimplePrice(ctx, gasTokenCoingeckoID, coingeckoUSDCurrency) - if err != nil { - return nil, fmt.Errorf("getting CoinGecko price of Ethereum in USD: %w", err) - } - - // Convert ETH price to microunits of USDC (uusdc) per Wei - // WEI_PER_ETH = 1_000_000_000_000_000_000 (1 ETH = 10^18 Wei) - // UUSDC_PER_USD = 1_000_000 (1 USD = 10^6 UUSDC) - const WEI_PER_ETH = 1_000_000_000_000_000_000 - const UUSDC_PER_USD = 1_000_000 - - // convert eth price in usd to eth price in uusdc - ethPriceUUSDC := new(big.Float).Mul(big.NewFloat(ethPriceUSD), new(big.Float).SetInt64(UUSDC_PER_USD)) - - // eth price in usd comes back from coin gecko with two decimals. Since we - // just converted to uusdc, shifting the decimal place right by 6, we can - // safely turn this into an int now - ethPriceUUSDCInt, ok := new(big.Int).SetString(ethPriceUUSDC.String(), 10) - if !ok { - return nil, fmt.Errorf("converting eth price in uusdc %s to *big.Int", ethPriceUUSDC.String()) - } - - // What we are really trying to do is: - // eth price uusdc / wei per eth = wei price in uusdc - // wei price in uusdc * tx fee wei = tx fee uusdc - // However we are choosing to first multiply eth price uusdc by tx fee wei - // so that we can do integer division when converting to wei, since if we - // first do integer division (before multiplying), we are going to cut off - // necessary decimals. there are limits of this, if eth price uusdc * tx - // fee wei has less than 9 digits, then we will just return 0. However, - // this is unlikely in practice and the tx fee would be very small if this - // is the case. - tmp := new(big.Int).Mul(ethPriceUUSDCInt, txFeeWei) - txFeeUUSDC := new(big.Int).Div(tmp, big.NewInt(WEI_PER_ETH)) - return txFeeUUSDC, nil -} diff --git a/shared/oracle/tx_price_oracle.go b/shared/oracle/tx_price_oracle.go index ab48b61..0c5c620 100644 --- a/shared/oracle/tx_price_oracle.go +++ b/shared/oracle/tx_price_oracle.go @@ -14,6 +14,16 @@ const ( coingeckoUSDCurrency = "usd" ) +type TxPriceOracle interface { + // TxFeeUUSDC estimates what the cost in uusdc would be to execute a tx. The + // tx's gas fee cap and gas limit must be set. + TxFeeUUSDC(ctx context.Context, tx *types.Transaction) (*big.Int, error) + + // GasCostUUSDC converts a tx fee to uusdc based on the current CoinGecko of + // the gas token in usd. + GasCostUUSDC(ctx context.Context, txFee *big.Int, chainID string) (*big.Int, error) +} + // Oracle is a evm uusdc tx execution price oracle that determines the price of // executing a tx on chain in uusdc. type Oracle struct { From 44a9465bd4e1a6a95dc7a722e5deadfbbe947d55 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Mon, 2 Dec 2024 16:17:05 -0500 Subject: [PATCH 12/20] add db queries to support fund rebalancer to add txns to submitted txs table --- db/gen/db/querier.go | 5 +- db/gen/db/rebalance_transactions.sql.go | 114 ++++++++++++++++++------ db/queries/rebalance_transactions.sql | 19 +++- 3 files changed, 103 insertions(+), 35 deletions(-) diff --git a/db/gen/db/querier.go b/db/gen/db/querier.go index 83d8861..1778f5c 100644 --- a/db/gen/db/querier.go +++ b/db/gen/db/querier.go @@ -17,19 +17,20 @@ type Querier interface { GetHyperlaneTransferByMessageSentTx(ctx context.Context, arg GetHyperlaneTransferByMessageSentTxParams) (HyperlaneTransfer, error) GetOrderByOrderID(ctx context.Context, orderID string) (Order, error) GetOrderSettlement(ctx context.Context, arg GetOrderSettlementParams) (OrderSettlement, error) + GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg GetPendingRebalanceTransfersBetweenChainsParams) ([]GetPendingRebalanceTransfersBetweenChainsRow, error) GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]GetPendingRebalanceTransfersToChainRow, error) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hyperlaneTransferID sql.NullInt64) ([]SubmittedTx, error) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSubmittedTxsByOrderIdAndTypeParams) ([]SubmittedTx, error) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg GetSubmittedTxsByOrderStatusAndTypeParams) ([]SubmittedTx, error) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string) ([]SubmittedTx, error) GetTransferMonitorMetadata(ctx context.Context, chainID string) (TransferMonitorMetadatum, error) + InitializeRebalanceTransfer(ctx context.Context, arg InitializeRebalanceTransferParams) (int64, error) InsertHyperlaneTransfer(ctx context.Context, arg InsertHyperlaneTransferParams) (HyperlaneTransfer, error) InsertOrder(ctx context.Context, arg InsertOrderParams) (Order, error) InsertOrderSettlement(ctx context.Context, arg InsertOrderSettlementParams) (OrderSettlement, error) InsertRebalanceTransfer(ctx context.Context, arg InsertRebalanceTransferParams) (int64, error) InsertSubmittedTx(ctx context.Context, arg InsertSubmittedTxParams) (SubmittedTx, error) InsertTransferMonitorMetadata(ctx context.Context, arg InsertTransferMonitorMetadataParams) (TransferMonitorMetadatum, error) - InsertUnsentRebalanceTransfer(ctx context.Context, arg InsertUnsentRebalanceTransferParams) (int64, error) SetCompleteSettlementTx(ctx context.Context, arg SetCompleteSettlementTxParams) (OrderSettlement, error) SetFillTx(ctx context.Context, arg SetFillTxParams) (Order, error) SetInitiateSettlementTx(ctx context.Context, arg SetInitiateSettlementTxParams) (OrderSettlement, error) @@ -38,8 +39,8 @@ type Querier interface { SetRefundTx(ctx context.Context, arg SetRefundTxParams) (Order, error) SetSettlementStatus(ctx context.Context, arg SetSettlementStatusParams) (OrderSettlement, error) SetSubmittedTxStatus(ctx context.Context, arg SetSubmittedTxStatusParams) (SubmittedTx, error) + UpdateTransfer(ctx context.Context, arg UpdateTransferParams) error UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error - UpdateTransferTxHash(ctx context.Context, arg UpdateTransferTxHashParams) error } var _ Querier = (*Queries)(nil) diff --git a/db/gen/db/rebalance_transactions.sql.go b/db/gen/db/rebalance_transactions.sql.go index bb351c8..b8b8aa6 100644 --- a/db/gen/db/rebalance_transactions.sql.go +++ b/db/gen/db/rebalance_transactions.sql.go @@ -61,6 +61,62 @@ func (q *Queries) GetAllPendingRebalanceTransfers(ctx context.Context) ([]GetAll return items, nil } +const getPendingRebalanceTransfersBetweenChains = `-- name: GetPendingRebalanceTransfersBetweenChains :many +SELECT + id, + tx_hash, + source_chain_id, + destination_chain_id, + amount, + created_at +FROM rebalance_transfers +WHERE status = 'PENDING' AND source_chain_id = ? AND destination_chain_id = ? +` + +type GetPendingRebalanceTransfersBetweenChainsParams struct { + SourceChainID string + DestinationChainID string +} + +type GetPendingRebalanceTransfersBetweenChainsRow struct { + ID int64 + TxHash string + SourceChainID string + DestinationChainID string + Amount string + CreatedAt time.Time +} + +func (q *Queries) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg GetPendingRebalanceTransfersBetweenChainsParams) ([]GetPendingRebalanceTransfersBetweenChainsRow, error) { + rows, err := q.db.QueryContext(ctx, getPendingRebalanceTransfersBetweenChains, arg.SourceChainID, arg.DestinationChainID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPendingRebalanceTransfersBetweenChainsRow + for rows.Next() { + var i GetPendingRebalanceTransfersBetweenChainsRow + if err := rows.Scan( + &i.ID, + &i.TxHash, + &i.SourceChainID, + &i.DestinationChainID, + &i.Amount, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPendingRebalanceTransfersToChain = `-- name: GetPendingRebalanceTransfersToChain :many SELECT id, @@ -109,84 +165,84 @@ func (q *Queries) GetPendingRebalanceTransfersToChain(ctx context.Context, desti return items, nil } -const insertRebalanceTransfer = `-- name: InsertRebalanceTransfer :one +const initializeRebalanceTransfer = `-- name: InitializeRebalanceTransfer :one INSERT INTO rebalance_transfers ( tx_hash, source_chain_id, destination_chain_id, amount -) VALUES (?, ?, ?, ?) RETURNING id +) VALUES ('', ?, ?, '0') RETURNING id ` -type InsertRebalanceTransferParams struct { - TxHash string +type InitializeRebalanceTransferParams struct { SourceChainID string DestinationChainID string - Amount string } -func (q *Queries) InsertRebalanceTransfer(ctx context.Context, arg InsertRebalanceTransferParams) (int64, error) { - row := q.db.QueryRowContext(ctx, insertRebalanceTransfer, - arg.TxHash, - arg.SourceChainID, - arg.DestinationChainID, - arg.Amount, - ) +func (q *Queries) InitializeRebalanceTransfer(ctx context.Context, arg InitializeRebalanceTransferParams) (int64, error) { + row := q.db.QueryRowContext(ctx, initializeRebalanceTransfer, arg.SourceChainID, arg.DestinationChainID) var id int64 err := row.Scan(&id) return id, err } -const insertUnsentRebalanceTransfer = `-- name: InsertUnsentRebalanceTransfer :one +const insertRebalanceTransfer = `-- name: InsertRebalanceTransfer :one INSERT INTO rebalance_transfers ( tx_hash, source_chain_id, destination_chain_id, amount -) VALUES ('', ?, ?, ?) RETURNING id +) VALUES (?, ?, ?, ?) RETURNING id ` -type InsertUnsentRebalanceTransferParams struct { +type InsertRebalanceTransferParams struct { + TxHash string SourceChainID string DestinationChainID string Amount string } -func (q *Queries) InsertUnsentRebalanceTransfer(ctx context.Context, arg InsertUnsentRebalanceTransferParams) (int64, error) { - row := q.db.QueryRowContext(ctx, insertUnsentRebalanceTransfer, arg.SourceChainID, arg.DestinationChainID, arg.Amount) +func (q *Queries) InsertRebalanceTransfer(ctx context.Context, arg InsertRebalanceTransferParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertRebalanceTransfer, + arg.TxHash, + arg.SourceChainID, + arg.DestinationChainID, + arg.Amount, + ) var id int64 err := row.Scan(&id) return id, err } -const updateTransferStatus = `-- name: UpdateTransferStatus :exec +const updateTransfer = `-- name: UpdateTransfer :exec UPDATE rebalance_transfers -SET updated_at=CURRENT_TIMESTAMP, status = ? +SET updated_at=CURRENT_TIMESTAMP, tx_hash = ?, amount = ? WHERE id = ? ` -type UpdateTransferStatusParams struct { - Status string +type UpdateTransferParams struct { + TxHash string + Amount string ID int64 } -func (q *Queries) UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error { - _, err := q.db.ExecContext(ctx, updateTransferStatus, arg.Status, arg.ID) +func (q *Queries) UpdateTransfer(ctx context.Context, arg UpdateTransferParams) error { + _, err := q.db.ExecContext(ctx, updateTransfer, arg.TxHash, arg.Amount, arg.ID) return err } -const updateTransferTxHash = `-- name: UpdateTransferTxHash :exec +const updateTransferStatus = `-- name: UpdateTransferStatus :exec UPDATE rebalance_transfers -SET updated_at=CURRENT_TIMESTAMP, tx_hash = ? +SET updated_at=CURRENT_TIMESTAMP, status = ? WHERE id = ? ` -type UpdateTransferTxHashParams struct { - TxHash string +type UpdateTransferStatusParams struct { + Status string ID int64 } -func (q *Queries) UpdateTransferTxHash(ctx context.Context, arg UpdateTransferTxHashParams) error { - _, err := q.db.ExecContext(ctx, updateTransferTxHash, arg.TxHash, arg.ID) +func (q *Queries) UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error { + _, err := q.db.ExecContext(ctx, updateTransferStatus, arg.Status, arg.ID) return err } diff --git a/db/queries/rebalance_transactions.sql b/db/queries/rebalance_transactions.sql index 78b2d44..3881103 100644 --- a/db/queries/rebalance_transactions.sql +++ b/db/queries/rebalance_transactions.sql @@ -33,15 +33,26 @@ UPDATE rebalance_transfers SET updated_at=CURRENT_TIMESTAMP, status = ? WHERE id = ?; --- name: UpdateTransferTxHash :exec +-- name: UpdateTransfer :exec UPDATE rebalance_transfers -SET updated_at=CURRENT_TIMESTAMP, tx_hash = ? +SET updated_at=CURRENT_TIMESTAMP, tx_hash = ?, amount = ? WHERE id = ?; --- name: InsertUnsentRebalanceTransfer :one +-- name: InitializeRebalanceTransfer :one INSERT INTO rebalance_transfers ( tx_hash, source_chain_id, destination_chain_id, amount -) VALUES ('', ?, ?, ?) RETURNING id; +) VALUES ('', ?, ?, '0') RETURNING id; + +-- name: GetPendingRebalanceTransfersBetweenChains :many +SELECT + id, + tx_hash, + source_chain_id, + destination_chain_id, + amount, + created_at +FROM rebalance_transfers +WHERE status = 'PENDING' AND source_chain_id = ? AND destination_chain_id = ?; From 8b0cb8ca6290b6f36a5205ac1dfa2733bec602a3 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Mon, 2 Dec 2024 16:17:18 -0500 Subject: [PATCH 13/20] fund rebalancer adds txs to submitted txs table --- fundrebalancer/fundrebalancer.go | 106 ++++++++------ fundrebalancer/fundrebalancer_test.go | 195 ++++++++++++++----------- fundrebalancer/transfermonitor_test.go | 3 +- 3 files changed, 177 insertions(+), 127 deletions(-) diff --git a/fundrebalancer/fundrebalancer.go b/fundrebalancer/fundrebalancer.go index 3ac22da..a0fe68d 100644 --- a/fundrebalancer/fundrebalancer.go +++ b/fundrebalancer/fundrebalancer.go @@ -9,6 +9,7 @@ import ( "time" "github.com/skip-mev/go-fast-solver/shared/keys" + "github.com/skip-mev/go-fast-solver/shared/oracle" evmtxsubmission "github.com/skip-mev/go-fast-solver/shared/txexecutor/evm" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -37,12 +38,13 @@ const ( type Database interface { GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]db.GetPendingRebalanceTransfersToChainRow, error) + GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) GetAllPendingRebalanceTransfers(ctx context.Context) ([]db.GetAllPendingRebalanceTransfersRow, error) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error InsertSubmittedTx(ctx context.Context, arg db.InsertSubmittedTxParams) (db.SubmittedTx, error) - UpdateTransferTxHash(ctx context.Context, arg db.UpdateTransferTxHashParams) error - InsertUnsentRebalanceTransfer(ctx context.Context, arg db.InsertUnsentRebalanceTransferParams) (int64, error) + UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error + InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) } type profitabilityFailure struct { @@ -58,7 +60,7 @@ type FundRebalancer struct { database Database trasferTracker *TransferTracker evmTxExecutor evmtxsubmission.EVMTxExecutor - evmTxPriceOracle evmrpc.IOracle + txPriceOracle oracle.TxPriceOracle profitabilityFailures map[string]*profitabilityFailure } @@ -68,7 +70,7 @@ func NewFundRebalancer( skipgo skipgo.SkipGoClient, evmClientManager evmrpc.EVMRPCClientManager, database Database, - evmTxPriceOracle evmrpc.IOracle, + txPriceOracle oracle.TxPriceOracle, evmTxExecutor evmtxsubmission.EVMTxExecutor, ) (*FundRebalancer, error) { return &FundRebalancer{ @@ -78,7 +80,7 @@ func NewFundRebalancer( config: config.GetConfigReader(ctx).Config().FundRebalancer, database: database, trasferTracker: NewTransferTracker(skipgo, database), - evmTxPriceOracle: evmTxPriceOracle, + txPriceOracle: txPriceOracle, evmTxExecutor: evmTxExecutor, profitabilityFailures: make(map[string]*profitabilityFailure), }, nil @@ -217,14 +219,14 @@ func (r *FundRebalancer) USDCNeeded( // rebalanceToChain. func (r *FundRebalancer) MoveFundsToChain( ctx context.Context, - rebalanceToChain string, + rebalanceToChainID string, usdcToReachTarget *big.Int, ) ([]skipgo.TxHash, *big.Int, error) { var hashes []skipgo.TxHash totalUSDCcMoved := big.NewInt(0) remainingUSDCNeeded := usdcToReachTarget for rebalanceFromChainID := range r.config { - if rebalanceFromChainID == rebalanceToChain { + if rebalanceFromChainID == rebalanceToChainID { // do not try and rebalance funds from the same chain continue } @@ -253,7 +255,7 @@ func (r *FundRebalancer) MoveFundsToChain( usdcToRebalance = usdcToSpare } - txns, err := r.GetRebalanceTxns(ctx, usdcToRebalance, rebalanceFromChainID, rebalanceToChain) + txns, err := r.GetRebalanceTxns(ctx, usdcToRebalance, rebalanceFromChainID, rebalanceToChainID) if err != nil { return nil, nil, fmt.Errorf("getting txns required for fund rebalancing: %w", err) } @@ -262,20 +264,26 @@ func (r *FundRebalancer) MoveFundsToChain( } txn := txns[0] - approvalHash, err := r.ApproveTxn(ctx, rebalanceFromChainID, txn) + rebalanceID, err := r.rebalanceID(ctx, rebalanceFromChainID, rebalanceToChainID) if err != nil { - return nil, nil, fmt.Errorf("approving rebalance txn from %s: %w", rebalanceFromChainID, err) + return nil, nil, fmt.Errorf("getting rebalance id between chains %s and %s: %w", rebalanceFromChainID, rebalanceToChainID, err) } - approveTx := db.InsertSubmittedTxParams{ - RebalanceTransferID: sql.NullInt64{Int64: rebalanceID, Valid: true}, - ChainID: chainID, - TxHash: hash, - TxType: dbtypes.TxTypeERC20Approval, - TxStatus: dbtypes.TxStatusPending, + approvalHash, err := r.ApproveTxn(ctx, rebalanceFromChainID, txn) + if err != nil { + return nil, nil, fmt.Errorf("approving rebalance txn from %s: %w", rebalanceFromChainID, err) } - if _, err = r.database.InsertSubmittedTx(ctx, approveTx); err != nil { - return "", fmt.Errorf("inserting submitted tx for erc20 approval with hash %s into db: %w", hash, err) + if approvalHash != "" { + approveTx := db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: rebalanceID, Valid: true}, + ChainID: rebalanceFromChainID, + TxHash: approvalHash, + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, + } + if _, err = r.database.InsertSubmittedTx(ctx, approveTx); err != nil { + return nil, nil, fmt.Errorf("inserting submitted tx for erc20 approval with hash %s on chain %s into db: %w", approvalHash, rebalanceFromChainID, err) + } } txnWithMetadata, err := r.TxnWithMetadata(ctx, rebalanceFromChainID, rebalanceFromChainID, usdcToRebalance, txn) @@ -300,7 +308,7 @@ func (r *FundRebalancer) MoveFundsToChain( } lmt.Logger(ctx).Info( "skipping rebalance from chain "+rebalanceFromChainID+" due to high rebalancing gas cost", - zap.String("destinationChainID", rebalanceToChain), + zap.String("destinationChainID", rebalanceToChainID), zap.String("estimatedGasCostUUSDC", gasCostUUSDC), zap.String("maxRebalancingGasCostUUSDC", maxCost.String()), ) @@ -312,22 +320,16 @@ func (r *FundRebalancer) MoveFundsToChain( if err != nil { return nil, nil, fmt.Errorf("signing and submitting transaction: %w", err) } - metrics.FromContext(ctx).IncFundsRebalanceTransferStatusChange(rebalanceFromChainID, rebalanceToChain, dbtypes.RebalanceTransferStatusPending) + metrics.FromContext(ctx).IncFundsRebalanceTransferStatusChange(rebalanceFromChainID, rebalanceToChainID, dbtypes.RebalanceTransferStatusPending) // add rebalance transfer to the db - rebalanceTransfer := db.InsertRebalanceTransferParams{ - TxHash: string(rebalanceHash), - SourceChainID: txnWithMetadata.sourceChainID, - DestinationChainID: txnWithMetadata.destinationChainID, - Amount: txnWithMetadata.amount.String(), + rebalanceTransfer := db.UpdateTransferParams{ + TxHash: string(rebalanceHash), + Amount: txnWithMetadata.amount.String(), + ID: rebalanceID, } - rebalanceID, err := r.database.InsertRebalanceTransfer(ctx, rebalanceTransfer) - if err != nil { - return nil, nil, fmt.Errorf("inserting rebalance transfer with hash %s into db: %w", rebalanceHash, err) - } - - // add erc20 approval tx to submitted txs table - if approvalHash != "" { + if err := r.database.UpdateTransfer(ctx, rebalanceTransfer); err != nil { + return nil, nil, fmt.Errorf("updating rebalance transfer with hash %s: %w", string(rebalanceHash), err) } // add rebalance tx to submitted txs table @@ -356,6 +358,33 @@ func (r *FundRebalancer) MoveFundsToChain( return hashes, totalUSDCcMoved, nil } +func (r *FundRebalancer) rebalanceID( + ctx context.Context, + rebalanceFromChainID string, + rebalanceToChainID string, +) (int64, error) { + chains := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: rebalanceFromChainID, DestinationChainID: rebalanceToChainID} + transfers, err := r.database.GetPendingRebalanceTransfersBetweenChains(ctx, chains) + if err != nil { + return 0, fmt.Errorf("getting pending rebalance transfers between chains source chain %s and destination chain %s: %w", rebalanceFromChainID, rebalanceToChainID, err) + } + + switch len(transfers) { + case 0: + // this is the first time we are seeing this rebalance, initialize it in the db + rebalance := db.InitializeRebalanceTransferParams{SourceChainID: rebalanceFromChainID, DestinationChainID: rebalanceToChainID} + id, err := r.database.InitializeRebalanceTransfer(ctx, rebalance) + if err != nil { + return 0, fmt.Errorf("initializing rebalance transfer from %s to %s in db: %w", rebalanceFromChainID, rebalanceToChainID, err) + } + return id, nil + case 1: + return transfers[0].ID, nil + default: + return 0, fmt.Errorf("expected to find 0 or 1 rebalance transfers between chains but instead found %d", len(transfers)) + } +} + func (r *FundRebalancer) ApproveTxn( ctx context.Context, chainID string, @@ -639,11 +668,11 @@ func (r *FundRebalancer) NeedsERC20Approval( ctx context.Context, txn skipgo.Tx, ) (bool, error) { - if txn.tx.EVMTx == nil { + if txn.EVMTx == nil { // if this isnt an evm tx, no erc20 approvals are required return false, nil } - evmTx := txn.tx.EVMTx + evmTx := txn.EVMTx if len(evmTx.RequiredERC20Approvals) == 0 { // if no approvals are required, return with no error return false, nil @@ -773,20 +802,15 @@ func (r *FundRebalancer) isGasAcceptable(ctx context.Context, txn SkipGoTxnWithM return false, "", fmt.Errorf("getting gas price: %w", err) } - chainConfig, err := config.GetConfigReader(ctx).GetChainConfig(chainID) - if err != nil { - return false, "", fmt.Errorf("getting chain config: %w", err) - } - chainFundRebalancingConfig, err := config.GetConfigReader(ctx).GetFundRebalancingConfig(chainID) if err != nil { return false, "", fmt.Errorf("getting chain fund rebalancing config: %w", err) } - gasCostUUSDC, err := r.evmTxPriceOracle.TxFeeUUSDC(ctx, types.NewTx(&types.DynamicFeeTx{ + gasCostUUSDC, err := r.txPriceOracle.TxFeeUUSDC(ctx, types.NewTx(&types.DynamicFeeTx{ Gas: txn.gasEstimate, GasFeeCap: gasPrice, - }), chainConfig.GasTokenCoingeckoID) + })) if err != nil { return false, "", fmt.Errorf("calculating total fund rebalancing gas cost in UUSDC: %w", err) } diff --git a/fundrebalancer/fundrebalancer_test.go b/fundrebalancer/fundrebalancer_test.go index 9ece5ac..c62564f 100644 --- a/fundrebalancer/fundrebalancer_test.go +++ b/fundrebalancer/fundrebalancer_test.go @@ -2,6 +2,7 @@ package fundrebalancer import ( "context" + "database/sql" "encoding/json" "fmt" "math/big" @@ -14,11 +15,14 @@ import ( "github.com/ethereum/go-ethereum/common" evm2 "github.com/skip-mev/go-fast-solver/mocks/shared/txexecutor/evm" + dbtypes "github.com/skip-mev/go-fast-solver/db" + "github.com/skip-mev/go-fast-solver/db/gen/db" mock_database "github.com/skip-mev/go-fast-solver/mocks/fundrebalancer" mock_skipgo "github.com/skip-mev/go-fast-solver/mocks/shared/clients/skipgo" mock_config "github.com/skip-mev/go-fast-solver/mocks/shared/config" mock_evmrpc "github.com/skip-mev/go-fast-solver/mocks/shared/evmrpc" + mock_oracle "github.com/skip-mev/go-fast-solver/mocks/shared/oracle" "github.com/skip-mev/go-fast-solver/shared/clients/skipgo" "github.com/skip-mev/go-fast-solver/shared/config" "github.com/skip-mev/go-fast-solver/shared/contracts/usdc" @@ -96,8 +100,6 @@ func loadKeysFile(keys any) (*os.File, error) { func TestFundRebalancer_Rebalance(t *testing.T) { t.Run("no rebalancing necessary", func(t *testing.T) { - t.Parallel() - ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -147,7 +149,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) rebalancer, err := NewFundRebalancer(ctx, keystore, mockSkipGo, mockEVMClientManager, mockDatabse, mockTxPriceOracle, mockEVMTxExecutor) assert.NoError(t, err) @@ -167,9 +169,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { // submit a rebalance txn, etc }) - t.Run("single arbitrum -> osmosis rebalance necessary", func(t *testing.T) { - t.Parallel() - + t.Run("single arbitrum to osmosis rebalance necessary", func(t *testing.T) { ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -230,10 +230,20 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(100), nil) mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) mockDatabse := mock_database.NewMockDatabase(t) + + pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) + + // basic initialization of a rebalance transfer + init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) + mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(75), nil) + keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) @@ -264,20 +274,22 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil).Once() - // should insert once rebalance transaction from arbitrum to osmosis - mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ - TxHash: "arbitrum hash", - SourceChainID: arbitrumChainID, - DestinationChainID: osmosisChainID, - Amount: strconv.Itoa(osmosisTargetAmount), - }).Return(1, nil).Once() + update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} + mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + + // insert tx into submitted txs table + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum hash", + TxType: dbtypes.TxTypeFundRebalnance, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Once() rebalancer.Rebalance(ctx) }) - t.Run("arbitrum + ethereum -> osmosis rebalance", func(t *testing.T) { - t.Parallel() - + t.Run("arbitrum and ethereum to osmosis rebalance", func(t *testing.T) { ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -351,7 +363,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, "42161", arbitrumAddress, []byte{}, "0", osmosisAddress, mock.Anything).Return("arbhash", nil) mockEVMTxExecutor.On("ExecuteTx", mockContext, "1", ethAddress, []byte{}, "0", osmosisAddress, mock.Anything).Return("ethhash", nil) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) // using an in memory database for this test mockDatabse := mock_database.NewFakeDatabase() @@ -421,8 +433,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { }) t.Run("in flight transfers are counted towards balance", func(t *testing.T) { - t.Parallel() - ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -464,7 +474,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClientManager := mock_evmrpc.NewMockEVMRPCClientManager(t) mockDatabse := mock_database.NewMockDatabase(t) mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) @@ -491,7 +501,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { }) t.Run("skips rebalance when gas threshold exceeded and timeout is set to -1", func(t *testing.T) { - t.Parallel() ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -549,10 +558,21 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) // high gas price mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) + mockDatabse := mock_database.NewMockDatabase(t) + // no pending rebalance transfers already in db + pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) + + // basic initialization of a rebalance transfer + init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) + mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(51), nil) + keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) @@ -595,8 +615,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { }) t.Run("submits required erc20 approvals returned from Skip Go", func(t *testing.T) { - t.Parallel() - ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -659,16 +677,31 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockDatabse := mock_database.NewMockDatabase(t) + pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) + + // basic initialization of a rebalance transfer + init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) + mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) // mock executing the approval tx - mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, mock.Anything, "0", arbitrumUSDCDenom, mock.Anything).Return("arbitrum hash", nil) + mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, mock.Anything, "0", arbitrumUSDCDenom, mock.Anything).Return("arbitrum approval hash", nil) + + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum approval hash", + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Once() keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) rebalancer, err := NewFundRebalancer(ctx, keystore, mockSkipGo, mockEVMClientManager, mockDatabse, mockTxPriceOracle, mockEVMTxExecutor) assert.NoError(t, err) @@ -708,20 +741,22 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) - // should insert once rebalance transaction from arbitrum to osmosis - mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ - TxHash: "arbitrum hash", - SourceChainID: arbitrumChainID, - DestinationChainID: osmosisChainID, - Amount: strconv.Itoa(osmosisTargetAmount), - }).Return(1, nil).Once() + update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} + mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + + // insert tx into submitted txs table + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum hash", + TxType: dbtypes.TxTypeFundRebalnance, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Once() rebalancer.Rebalance(ctx) }) t.Run("does not submit erc20 approval when erc20 allowance is greater than necessary approval", func(t *testing.T) { - t.Parallel() - ctx := context.Background() mockConfigReader := mock_config.NewMockConfigReader(t) mockConfigReader.On("Config").Return(config.Config{ @@ -783,6 +818,12 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.EXPECT().CallContract(mock.Anything, msg, nilBigInt).Return(common.LeftPadBytes(big.NewInt(10000).Bytes(), 32), nil) mockDatabse := mock_database.NewMockDatabase(t) + pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) + + // basic initialization of a rebalance transfer + init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) @@ -790,7 +831,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) rebalancer, err := NewFundRebalancer(ctx, keystore, mockSkipGo, mockEVMClientManager, mockDatabse, mockTxPriceOracle, mockEVMTxExecutor) assert.NoError(t, err) @@ -830,13 +871,18 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) - // should insert once rebalance transaction from arbitrum to osmosis - mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ - TxHash: "arbitrum hash", - SourceChainID: arbitrumChainID, - DestinationChainID: osmosisChainID, - Amount: strconv.Itoa(osmosisTargetAmount), - }).Return(1, nil).Once() + // update the initialized tx with hash and amount + update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} + mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + + // insert tx into submitted txs table + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum hash", + TxType: dbtypes.TxTypeFundRebalnance, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Once() rebalancer.Rebalance(ctx) }) @@ -844,7 +890,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { func TestFundRebalancer_GasAcceptability(t *testing.T) { t.Run("accepts transaction above threshold but below cap after timeout", func(t *testing.T) { - t.Parallel() ctx := context.Background() mockContext := mock.Anything timeout := 1 * time.Hour @@ -870,28 +915,22 @@ func TestFundRebalancer_GasAcceptability(t *testing.T) { }, nil, ) - mockConfigReader.On("GetChainConfig", arbitrumChainID).Return( - config.ChainConfig{ - Type: config.ChainType_EVM, - }, - nil, - ) ctx = config.ConfigReaderContext(ctx, mockConfigReader) mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) - mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(75), nil) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) + mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(75), nil) rebalancer := setupRebalancer(t, ctx, mockEVMClient, mockTxPriceOracle, nil) - txns := []SkipGoTxnWithMetadata{{ + txn := SkipGoTxnWithMetadata{ tx: skipgo.Tx{EVMTx: &skipgo.EVMTx{ChainID: arbitrumChainID}}, gasEstimate: 100000, - }} + } // First attempt should fail and start tracking - acceptable, cost, err := rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, cost, err := rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.False(t, acceptable) assert.Equal(t, "75", cost) @@ -900,14 +939,13 @@ func TestFundRebalancer_GasAcceptability(t *testing.T) { rebalancer.profitabilityFailures[arbitrumChainID].firstFailureTime = time.Now().Add(-2 * time.Hour) // Second attempt should succeed due to timeout - acceptable, cost, err = rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, cost, err = rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.True(t, acceptable) assert.Equal(t, "75", cost) }) t.Run("rejects transaction above cap even after timeout", func(t *testing.T) { - t.Parallel() ctx := context.Background() mockContext := mock.Anything timeout := 1 * time.Hour @@ -933,28 +971,22 @@ func TestFundRebalancer_GasAcceptability(t *testing.T) { }, nil, ) - mockConfigReader.On("GetChainConfig", arbitrumChainID).Return( - config.ChainConfig{ - Type: config.ChainType_EVM, - }, - nil, - ) ctx = config.ConfigReaderContext(ctx, mockConfigReader) mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) - mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(150), nil) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) + mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(150), nil) rebalancer := setupRebalancer(t, ctx, mockEVMClient, mockTxPriceOracle, nil) - txns := []SkipGoTxnWithMetadata{{ + txn := SkipGoTxnWithMetadata{ tx: skipgo.Tx{EVMTx: &skipgo.EVMTx{ChainID: arbitrumChainID}}, gasEstimate: 100000, - }} + } // First attempt should fail and start tracking - acceptable, cost, err := rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, cost, err := rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.False(t, acceptable) assert.Equal(t, "150", cost) @@ -963,14 +995,13 @@ func TestFundRebalancer_GasAcceptability(t *testing.T) { rebalancer.profitabilityFailures[arbitrumChainID].firstFailureTime = time.Now().Add(-2 * time.Hour) // Second attempt should still fail due to being above cap - acceptable, cost, err = rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, cost, err = rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.False(t, acceptable) assert.Equal(t, "150", cost) }) t.Run("clears failure tracking when gas becomes acceptable", func(t *testing.T) { - t.Parallel() ctx := context.Background() mockContext := mock.Anything timeout := 1 * time.Hour @@ -996,44 +1027,38 @@ func TestFundRebalancer_GasAcceptability(t *testing.T) { }, nil, ) - mockConfigReader.On("GetChainConfig", arbitrumChainID).Return( - config.ChainConfig{ - Type: config.ChainType_EVM, - }, - nil, - ) ctx = config.ConfigReaderContext(ctx, mockConfigReader) mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) rebalancer := setupRebalancer(t, ctx, mockEVMClient, mockTxPriceOracle, nil) - txns := []SkipGoTxnWithMetadata{{ + txn := SkipGoTxnWithMetadata{ tx: skipgo.Tx{EVMTx: &skipgo.EVMTx{ChainID: arbitrumChainID}}, gasEstimate: 100000, - }} + } // First attempt with high gas mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) - mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(75), nil).Once() + mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(75), nil).Once() - acceptable, _, err := rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, _, err := rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.False(t, acceptable) assert.NotNil(t, rebalancer.profitabilityFailures[arbitrumChainID]) // Second attempt with low gas mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(500000000), nil) - mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything, mock.Anything).Return(big.NewInt(25), nil).Once() + mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(25), nil).Once() - acceptable, _, err = rebalancer.isGasAcceptable(ctx, txns, arbitrumChainID) + acceptable, _, err = rebalancer.isGasAcceptable(ctx, txn, arbitrumChainID) assert.NoError(t, err) assert.True(t, acceptable) assert.Nil(t, rebalancer.profitabilityFailures[arbitrumChainID]) }) } -func setupRebalancer(t *testing.T, ctx context.Context, mockEVMClient *mock_evmrpc.MockEVMChainRPC, mockTxPriceOracle *mock_evmrpc.MockOracle, mockDatabase *mock_database.MockDatabase) *FundRebalancer { +func setupRebalancer(t *testing.T, ctx context.Context, mockEVMClient *mock_evmrpc.MockEVMChainRPC, mockTxPriceOracle *mock_oracle.MockTxPriceOracle, mockDatabase *mock_database.MockDatabase) *FundRebalancer { mockEVMClientManager := mock_evmrpc.NewMockEVMRPCClientManager(t) mockEVMClientManager.EXPECT().GetClient(mock.Anything, arbitrumChainID).Return(mockEVMClient, nil) mockSkipGo := mock_skipgo.NewMockSkipGoClient(t) diff --git a/fundrebalancer/transfermonitor_test.go b/fundrebalancer/transfermonitor_test.go index d05458a..6064bc9 100644 --- a/fundrebalancer/transfermonitor_test.go +++ b/fundrebalancer/transfermonitor_test.go @@ -12,6 +12,7 @@ import ( mock_skipgo "github.com/skip-mev/go-fast-solver/mocks/shared/clients/skipgo" mock_config "github.com/skip-mev/go-fast-solver/mocks/shared/config" mock_evmrpc "github.com/skip-mev/go-fast-solver/mocks/shared/evmrpc" + mock_oracle "github.com/skip-mev/go-fast-solver/mocks/shared/oracle" evm2 "github.com/skip-mev/go-fast-solver/mocks/shared/txexecutor/evm" "github.com/skip-mev/go-fast-solver/shared/clients/skipgo" "github.com/skip-mev/go-fast-solver/shared/config" @@ -175,7 +176,7 @@ func TestFundRebalancer_RebalanceWithAbandonedTransfer(t *testing.T) { mockEVMClientManager.EXPECT().GetClient(ctx, arbitrumChainID).Return(mockEVMClient, nil) fakeDatabase := mock_database.NewFakeDatabase() mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) - mockTxPriceOracle := mock_evmrpc.NewMockOracle(t) + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) mockTxPriceOracle.On("TxFeeUUSDC", ctx, mock.Anything, mock.Anything).Return(big.NewInt(75), nil) keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) assert.NoError(t, err) From 4d4f52fcd8cf822c2c2f8452044380720baff53f Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Mon, 2 Dec 2024 16:38:05 -0500 Subject: [PATCH 14/20] add test for fund rebalancer adding txns to submitted txns table --- fundrebalancer/fundrebalancer_test.go | 167 ++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/fundrebalancer/fundrebalancer_test.go b/fundrebalancer/fundrebalancer_test.go index c62564f..563ff2c 100644 --- a/fundrebalancer/fundrebalancer_test.go +++ b/fundrebalancer/fundrebalancer_test.go @@ -886,6 +886,173 @@ func TestFundRebalancer_Rebalance(t *testing.T) { rebalancer.Rebalance(ctx) }) + + t.Run("required erc20 approvals and gas too high, then timeout and tx goes through", func(t *testing.T) { + ctx := context.Background() + mockConfigReader := mock_config.NewMockConfigReader(t) + mockConfigReader.On("Config").Return(config.Config{ + FundRebalancer: map[string]config.FundRebalancerConfig{ + osmosisChainID: { + TargetAmount: strconv.Itoa(osmosisTargetAmount), + MinAllowedAmount: strconv.Itoa(osmosisMinAmount), + }, + arbitrumChainID: { + TargetAmount: strconv.Itoa(arbitrumTargetAmount), + MinAllowedAmount: strconv.Itoa(arbitrumMinAmount), + MaxRebalancingGasCostUUSDC: "50", + ProfitabilityTimeout: 1 * time.Hour, + TransferCostCapUUSDC: "100", + }, + }, + }) + mockConfigReader.On("GetFundRebalancingConfig", arbitrumChainID).Return( + config.FundRebalancerConfig{ + TargetAmount: strconv.Itoa(arbitrumTargetAmount), + MinAllowedAmount: strconv.Itoa(arbitrumMinAmount), + MaxRebalancingGasCostUUSDC: "50", + ProfitabilityTimeout: 1 * time.Hour, + TransferCostCapUUSDC: "100", + }, + nil, + ) + + mockConfigReader.EXPECT().GetUSDCDenom(osmosisChainID).Return(osmosisUSDCDenom, nil) + mockConfigReader.EXPECT().GetUSDCDenom(arbitrumChainID).Return(arbitrumUSDCDenom, nil) + mockConfigReader.On("GetChainConfig", osmosisChainID).Return( + config.ChainConfig{ + Type: config.ChainType_COSMOS, + USDCDenom: osmosisUSDCDenom, + SolverAddress: osmosisAddress, + }, + nil, + ) + mockConfigReader.On("GetChainConfig", arbitrumChainID).Return( + config.ChainConfig{ + Type: config.ChainType_EVM, + USDCDenom: arbitrumUSDCDenom, + SolverAddress: arbitrumAddress, + }, + nil, + ) + ctx = config.ConfigReaderContext(ctx, mockConfigReader) + + f, err := loadKeysFile(defaultKeys) + assert.NoError(t, err) + + mockSkipGo := mock_skipgo.NewMockSkipGoClient(t) + mockEVMClientManager := mock_evmrpc.NewMockEVMRPCClientManager(t) + mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) + mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) + + abi, err := usdc.UsdcMetaData.GetAbi() + assert.NoError(t, err) + data, err := abi.Pack("allowance", common.HexToAddress(arbitrumAddress), common.HexToAddress("0xskipgo")) + assert.NoError(t, err) + + to := common.HexToAddress(arbitrumUSDCDenom) + msg := ethereum.CallMsg{From: common.Address{}, To: &to, Data: data} + var nilBigInt *big.Int + mockEVMClient.EXPECT().CallContract(mock.Anything, msg, nilBigInt).Return(common.LeftPadBytes(big.NewInt(100).Bytes(), 32), nil) + + mockDatabse := mock_database.NewMockDatabase(t) + + pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) + + // basic initialization of a rebalance transfer + init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) + + mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) + mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) + + // mock executing the approval tx + mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, mock.Anything, "0", arbitrumUSDCDenom, mock.Anything).Return("arbitrum approval hash", nil) + + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum approval hash", + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Twice() + + keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) + assert.NoError(t, err) + + mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) + mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) + mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(75), nil) + + rebalancer, err := NewFundRebalancer(ctx, keystore, mockSkipGo, mockEVMClientManager, mockDatabse, mockTxPriceOracle, mockEVMTxExecutor) + assert.NoError(t, err) + + // setup initial state of mocks + + // no pending txns + mockDatabse.EXPECT().GetAllPendingRebalanceTransfers(mockContext).Return(nil, nil).Maybe() + mockDatabse.EXPECT().GetPendingRebalanceTransfersToChain(mockContext, osmosisChainID).Return(nil, nil) + + // osmosis balance lower than min amount, arbitrum & eth balances higher than target + mockSkipGo.EXPECT().Balance(mockContext, osmosisChainID, osmosisAddress, osmosisUSDCDenom).Return("0", nil) + mockEVMClient.EXPECT().GetUSDCBalance(mockContext, arbitrumUSDCDenom, arbitrumAddress).Return(big.NewInt(1000), nil) + + route := &skipgo.RouteResponse{ + AmountOut: strconv.Itoa(osmosisTargetAmount), + Operations: []any{"opts"}, + RequiredChainAddresses: []string{arbitrumChainID, osmosisChainID}, + } + mockSkipGo.EXPECT().Route(mockContext, arbitrumUSDCDenom, arbitrumChainID, osmosisUSDCDenom, osmosisChainID, big.NewInt(osmosisTargetAmount)). + Return(route, nil).Twice() + + txs := []skipgo.Tx{{ + EVMTx: &skipgo.EVMTx{ + ChainID: arbitrumChainID, + To: osmosisAddress, + Value: "999", + RequiredERC20Approvals: []skipgo.ERC20Approval{{ + TokenContract: arbitrumUSDCDenom, + Spender: "0xskipgo", + Amount: "999", + }}, + SignerAddress: arbitrumAddress, + }}} + mockSkipGo.EXPECT().Msgs(mockContext, arbitrumUSDCDenom, arbitrumChainID, arbitrumAddress, osmosisUSDCDenom, osmosisChainID, osmosisAddress, big.NewInt(osmosisTargetAmount), big.NewInt(osmosisTargetAmount), []string{arbitrumAddress, osmosisAddress}, route.Operations). + Return(txs, nil).Twice() + + mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) + + // this call to rebalance will see that the gas is not acceptable and not insert the rebalance tx + rebalancer.Rebalance(ctx) + + update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} + mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + + // insert tx into submitted txs table + mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ + RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, + ChainID: arbitrumChainID, + TxHash: "arbitrum hash", + TxType: dbtypes.TxTypeFundRebalnance, + TxStatus: dbtypes.TxStatusPending, + }).Return(db.SubmittedTx{}, nil).Once() + + // modify the failure so that it is now timed out and uses the timeout + // cost cap + rebalancer.profitabilityFailures[arbitrumChainID].firstFailureTime = time.Now().Add(-2 * time.Hour) + + // now return the previous rebalance transfer we inserted + pending = db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Unset() + mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return([]db.GetPendingRebalanceTransfersBetweenChainsRow{{ + ID: 0, + SourceChainID: arbitrumChainID, + DestinationChainID: osmosisChainID, + }}, nil) + + rebalancer.Rebalance(ctx) + + }) } func TestFundRebalancer_GasAcceptability(t *testing.T) { From c666e810e51c92793c4c0f71d8005a4705abcd87 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Mon, 2 Dec 2024 16:40:49 -0500 Subject: [PATCH 15/20] fix imports --- shared/bridges/cctp/cosmos_bridge_client.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/shared/bridges/cctp/cosmos_bridge_client.go b/shared/bridges/cctp/cosmos_bridge_client.go index 9fa0145..03fc3c0 100644 --- a/shared/bridges/cctp/cosmos_bridge_client.go +++ b/shared/bridges/cctp/cosmos_bridge_client.go @@ -12,9 +12,6 @@ import ( "strings" "time" - "github.com/skip-mev/go-fast-solver/shared/contracts/fast_transfer_gateway" - "github.com/skip-mev/go-fast-solver/shared/txexecutor/cosmos" - sdkgrpc "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/skip-mev/go-fast-solver/shared/contracts/fast_transfer_gateway" From 10825ff15f060d8d60e3b802f10943a75024658e Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 5 Dec 2024 15:04:04 -0500 Subject: [PATCH 16/20] dont link erc20 approval txs to rebalance txs in fund rebalancer --- db/gen/db/querier.go | 3 - db/gen/db/rebalance_transactions.sql.go | 94 ---------- db/queries/rebalance_transactions.sql | 24 --- fundrebalancer/fundrebalancer.go | 58 ++---- fundrebalancer/fundrebalancer_test.go | 228 +++--------------------- mocks/fundrebalancer/fake_database.go | 42 ----- mocks/fundrebalancer/mock_database.go | 163 ----------------- 7 files changed, 35 insertions(+), 577 deletions(-) diff --git a/db/gen/db/querier.go b/db/gen/db/querier.go index 1778f5c..c8c5dab 100644 --- a/db/gen/db/querier.go +++ b/db/gen/db/querier.go @@ -17,14 +17,12 @@ type Querier interface { GetHyperlaneTransferByMessageSentTx(ctx context.Context, arg GetHyperlaneTransferByMessageSentTxParams) (HyperlaneTransfer, error) GetOrderByOrderID(ctx context.Context, orderID string) (Order, error) GetOrderSettlement(ctx context.Context, arg GetOrderSettlementParams) (OrderSettlement, error) - GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg GetPendingRebalanceTransfersBetweenChainsParams) ([]GetPendingRebalanceTransfersBetweenChainsRow, error) GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]GetPendingRebalanceTransfersToChainRow, error) GetSubmittedTxsByHyperlaneTransferId(ctx context.Context, hyperlaneTransferID sql.NullInt64) ([]SubmittedTx, error) GetSubmittedTxsByOrderIdAndType(ctx context.Context, arg GetSubmittedTxsByOrderIdAndTypeParams) ([]SubmittedTx, error) GetSubmittedTxsByOrderStatusAndType(ctx context.Context, arg GetSubmittedTxsByOrderStatusAndTypeParams) ([]SubmittedTx, error) GetSubmittedTxsWithStatus(ctx context.Context, txStatus string) ([]SubmittedTx, error) GetTransferMonitorMetadata(ctx context.Context, chainID string) (TransferMonitorMetadatum, error) - InitializeRebalanceTransfer(ctx context.Context, arg InitializeRebalanceTransferParams) (int64, error) InsertHyperlaneTransfer(ctx context.Context, arg InsertHyperlaneTransferParams) (HyperlaneTransfer, error) InsertOrder(ctx context.Context, arg InsertOrderParams) (Order, error) InsertOrderSettlement(ctx context.Context, arg InsertOrderSettlementParams) (OrderSettlement, error) @@ -39,7 +37,6 @@ type Querier interface { SetRefundTx(ctx context.Context, arg SetRefundTxParams) (Order, error) SetSettlementStatus(ctx context.Context, arg SetSettlementStatusParams) (OrderSettlement, error) SetSubmittedTxStatus(ctx context.Context, arg SetSubmittedTxStatusParams) (SubmittedTx, error) - UpdateTransfer(ctx context.Context, arg UpdateTransferParams) error UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error } diff --git a/db/gen/db/rebalance_transactions.sql.go b/db/gen/db/rebalance_transactions.sql.go index b8b8aa6..517e854 100644 --- a/db/gen/db/rebalance_transactions.sql.go +++ b/db/gen/db/rebalance_transactions.sql.go @@ -61,62 +61,6 @@ func (q *Queries) GetAllPendingRebalanceTransfers(ctx context.Context) ([]GetAll return items, nil } -const getPendingRebalanceTransfersBetweenChains = `-- name: GetPendingRebalanceTransfersBetweenChains :many -SELECT - id, - tx_hash, - source_chain_id, - destination_chain_id, - amount, - created_at -FROM rebalance_transfers -WHERE status = 'PENDING' AND source_chain_id = ? AND destination_chain_id = ? -` - -type GetPendingRebalanceTransfersBetweenChainsParams struct { - SourceChainID string - DestinationChainID string -} - -type GetPendingRebalanceTransfersBetweenChainsRow struct { - ID int64 - TxHash string - SourceChainID string - DestinationChainID string - Amount string - CreatedAt time.Time -} - -func (q *Queries) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg GetPendingRebalanceTransfersBetweenChainsParams) ([]GetPendingRebalanceTransfersBetweenChainsRow, error) { - rows, err := q.db.QueryContext(ctx, getPendingRebalanceTransfersBetweenChains, arg.SourceChainID, arg.DestinationChainID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetPendingRebalanceTransfersBetweenChainsRow - for rows.Next() { - var i GetPendingRebalanceTransfersBetweenChainsRow - if err := rows.Scan( - &i.ID, - &i.TxHash, - &i.SourceChainID, - &i.DestinationChainID, - &i.Amount, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getPendingRebalanceTransfersToChain = `-- name: GetPendingRebalanceTransfersToChain :many SELECT id, @@ -165,27 +109,6 @@ func (q *Queries) GetPendingRebalanceTransfersToChain(ctx context.Context, desti return items, nil } -const initializeRebalanceTransfer = `-- name: InitializeRebalanceTransfer :one -INSERT INTO rebalance_transfers ( - tx_hash, - source_chain_id, - destination_chain_id, - amount -) VALUES ('', ?, ?, '0') RETURNING id -` - -type InitializeRebalanceTransferParams struct { - SourceChainID string - DestinationChainID string -} - -func (q *Queries) InitializeRebalanceTransfer(ctx context.Context, arg InitializeRebalanceTransferParams) (int64, error) { - row := q.db.QueryRowContext(ctx, initializeRebalanceTransfer, arg.SourceChainID, arg.DestinationChainID) - var id int64 - err := row.Scan(&id) - return id, err -} - const insertRebalanceTransfer = `-- name: InsertRebalanceTransfer :one INSERT INTO rebalance_transfers ( tx_hash, @@ -214,23 +137,6 @@ func (q *Queries) InsertRebalanceTransfer(ctx context.Context, arg InsertRebalan return id, err } -const updateTransfer = `-- name: UpdateTransfer :exec -UPDATE rebalance_transfers -SET updated_at=CURRENT_TIMESTAMP, tx_hash = ?, amount = ? -WHERE id = ? -` - -type UpdateTransferParams struct { - TxHash string - Amount string - ID int64 -} - -func (q *Queries) UpdateTransfer(ctx context.Context, arg UpdateTransferParams) error { - _, err := q.db.ExecContext(ctx, updateTransfer, arg.TxHash, arg.Amount, arg.ID) - return err -} - const updateTransferStatus = `-- name: UpdateTransferStatus :exec UPDATE rebalance_transfers SET updated_at=CURRENT_TIMESTAMP, status = ? diff --git a/db/queries/rebalance_transactions.sql b/db/queries/rebalance_transactions.sql index 3881103..997213b 100644 --- a/db/queries/rebalance_transactions.sql +++ b/db/queries/rebalance_transactions.sql @@ -32,27 +32,3 @@ WHERE status = 'PENDING'; UPDATE rebalance_transfers SET updated_at=CURRENT_TIMESTAMP, status = ? WHERE id = ?; - --- name: UpdateTransfer :exec -UPDATE rebalance_transfers -SET updated_at=CURRENT_TIMESTAMP, tx_hash = ?, amount = ? -WHERE id = ?; - --- name: InitializeRebalanceTransfer :one -INSERT INTO rebalance_transfers ( - tx_hash, - source_chain_id, - destination_chain_id, - amount -) VALUES ('', ?, ?, '0') RETURNING id; - --- name: GetPendingRebalanceTransfersBetweenChains :many -SELECT - id, - tx_hash, - source_chain_id, - destination_chain_id, - amount, - created_at -FROM rebalance_transfers -WHERE status = 'PENDING' AND source_chain_id = ? AND destination_chain_id = ?; diff --git a/fundrebalancer/fundrebalancer.go b/fundrebalancer/fundrebalancer.go index a0fe68d..d3e8a9e 100644 --- a/fundrebalancer/fundrebalancer.go +++ b/fundrebalancer/fundrebalancer.go @@ -38,13 +38,10 @@ const ( type Database interface { GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]db.GetPendingRebalanceTransfersToChainRow, error) - GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) GetAllPendingRebalanceTransfers(ctx context.Context) ([]db.GetAllPendingRebalanceTransfersRow, error) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error InsertSubmittedTx(ctx context.Context, arg db.InsertSubmittedTxParams) (db.SubmittedTx, error) - UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error - InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) } type profitabilityFailure struct { @@ -264,22 +261,18 @@ func (r *FundRebalancer) MoveFundsToChain( } txn := txns[0] - rebalanceID, err := r.rebalanceID(ctx, rebalanceFromChainID, rebalanceToChainID) - if err != nil { - return nil, nil, fmt.Errorf("getting rebalance id between chains %s and %s: %w", rebalanceFromChainID, rebalanceToChainID, err) - } - approvalHash, err := r.ApproveTxn(ctx, rebalanceFromChainID, txn) if err != nil { return nil, nil, fmt.Errorf("approving rebalance txn from %s: %w", rebalanceFromChainID, err) } if approvalHash != "" { + // not we are not linking this submitted tx to the rebalance since the rebalance + // has not yet been created approveTx := db.InsertSubmittedTxParams{ - RebalanceTransferID: sql.NullInt64{Int64: rebalanceID, Valid: true}, - ChainID: rebalanceFromChainID, - TxHash: approvalHash, - TxType: dbtypes.TxTypeERC20Approval, - TxStatus: dbtypes.TxStatusPending, + ChainID: rebalanceFromChainID, + TxHash: approvalHash, + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, } if _, err = r.database.InsertSubmittedTx(ctx, approveTx); err != nil { return nil, nil, fmt.Errorf("inserting submitted tx for erc20 approval with hash %s on chain %s into db: %w", approvalHash, rebalanceFromChainID, err) @@ -323,12 +316,14 @@ func (r *FundRebalancer) MoveFundsToChain( metrics.FromContext(ctx).IncFundsRebalanceTransferStatusChange(rebalanceFromChainID, rebalanceToChainID, dbtypes.RebalanceTransferStatusPending) // add rebalance transfer to the db - rebalanceTransfer := db.UpdateTransferParams{ - TxHash: string(rebalanceHash), - Amount: txnWithMetadata.amount.String(), - ID: rebalanceID, + rebalanceTransfer := db.InsertRebalanceTransferParams{ + TxHash: string(rebalanceHash), + Amount: txnWithMetadata.amount.String(), + SourceChainID: rebalanceFromChainID, + DestinationChainID: rebalanceToChainID, } - if err := r.database.UpdateTransfer(ctx, rebalanceTransfer); err != nil { + rebalanceID, err := r.database.InsertRebalanceTransfer(ctx, rebalanceTransfer) + if err != nil { return nil, nil, fmt.Errorf("updating rebalance transfer with hash %s: %w", string(rebalanceHash), err) } @@ -358,33 +353,6 @@ func (r *FundRebalancer) MoveFundsToChain( return hashes, totalUSDCcMoved, nil } -func (r *FundRebalancer) rebalanceID( - ctx context.Context, - rebalanceFromChainID string, - rebalanceToChainID string, -) (int64, error) { - chains := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: rebalanceFromChainID, DestinationChainID: rebalanceToChainID} - transfers, err := r.database.GetPendingRebalanceTransfersBetweenChains(ctx, chains) - if err != nil { - return 0, fmt.Errorf("getting pending rebalance transfers between chains source chain %s and destination chain %s: %w", rebalanceFromChainID, rebalanceToChainID, err) - } - - switch len(transfers) { - case 0: - // this is the first time we are seeing this rebalance, initialize it in the db - rebalance := db.InitializeRebalanceTransferParams{SourceChainID: rebalanceFromChainID, DestinationChainID: rebalanceToChainID} - id, err := r.database.InitializeRebalanceTransfer(ctx, rebalance) - if err != nil { - return 0, fmt.Errorf("initializing rebalance transfer from %s to %s in db: %w", rebalanceFromChainID, rebalanceToChainID, err) - } - return id, nil - case 1: - return transfers[0].ID, nil - default: - return 0, fmt.Errorf("expected to find 0 or 1 rebalance transfers between chains but instead found %d", len(transfers)) - } -} - func (r *FundRebalancer) ApproveTxn( ctx context.Context, chainID string, diff --git a/fundrebalancer/fundrebalancer_test.go b/fundrebalancer/fundrebalancer_test.go index 563ff2c..c971000 100644 --- a/fundrebalancer/fundrebalancer_test.go +++ b/fundrebalancer/fundrebalancer_test.go @@ -231,13 +231,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) mockDatabse := mock_database.NewMockDatabase(t) - pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) - - // basic initialization of a rebalance transfer - init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) - mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) @@ -274,8 +267,12 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil).Once() - update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} - mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ + TxHash: "arbitrum hash", + SourceChainID: arbitrumChainID, + DestinationChainID: osmosisChainID, + Amount: "100", + }).Return(0, nil) // insert tx into submitted txs table mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ @@ -560,13 +557,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) mockDatabse := mock_database.NewMockDatabase(t) - // no pending rebalance transfers already in db - pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) - - // basic initialization of a rebalance transfer - init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) @@ -677,13 +667,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockDatabse := mock_database.NewMockDatabase(t) - pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) - - // basic initialization of a rebalance transfer - init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) - mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) @@ -691,11 +674,10 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, mock.Anything, "0", arbitrumUSDCDenom, mock.Anything).Return("arbitrum approval hash", nil) mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ - RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, - ChainID: arbitrumChainID, - TxHash: "arbitrum approval hash", - TxType: dbtypes.TxTypeERC20Approval, - TxStatus: dbtypes.TxStatusPending, + ChainID: arbitrumChainID, + TxHash: "arbitrum approval hash", + TxType: dbtypes.TxTypeERC20Approval, + TxStatus: dbtypes.TxStatusPending, }).Return(db.SubmittedTx{}, nil).Once() keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) @@ -741,8 +723,12 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) - update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} - mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ + TxHash: "arbitrum hash", + SourceChainID: arbitrumChainID, + DestinationChainID: osmosisChainID, + Amount: strconv.Itoa(osmosisTargetAmount), + }).Return(0, nil) // insert tx into submitted txs table mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ @@ -818,12 +804,6 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.EXPECT().CallContract(mock.Anything, msg, nilBigInt).Return(common.LeftPadBytes(big.NewInt(10000).Bytes(), 32), nil) mockDatabse := mock_database.NewMockDatabase(t) - pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) - - // basic initialization of a rebalance transfer - init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) @@ -871,162 +851,12 @@ func TestFundRebalancer_Rebalance(t *testing.T) { mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) - // update the initialized tx with hash and amount - update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} - mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) - - // insert tx into submitted txs table - mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ - RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, - ChainID: arbitrumChainID, - TxHash: "arbitrum hash", - TxType: dbtypes.TxTypeFundRebalnance, - TxStatus: dbtypes.TxStatusPending, - }).Return(db.SubmittedTx{}, nil).Once() - - rebalancer.Rebalance(ctx) - }) - - t.Run("required erc20 approvals and gas too high, then timeout and tx goes through", func(t *testing.T) { - ctx := context.Background() - mockConfigReader := mock_config.NewMockConfigReader(t) - mockConfigReader.On("Config").Return(config.Config{ - FundRebalancer: map[string]config.FundRebalancerConfig{ - osmosisChainID: { - TargetAmount: strconv.Itoa(osmosisTargetAmount), - MinAllowedAmount: strconv.Itoa(osmosisMinAmount), - }, - arbitrumChainID: { - TargetAmount: strconv.Itoa(arbitrumTargetAmount), - MinAllowedAmount: strconv.Itoa(arbitrumMinAmount), - MaxRebalancingGasCostUUSDC: "50", - ProfitabilityTimeout: 1 * time.Hour, - TransferCostCapUUSDC: "100", - }, - }, - }) - mockConfigReader.On("GetFundRebalancingConfig", arbitrumChainID).Return( - config.FundRebalancerConfig{ - TargetAmount: strconv.Itoa(arbitrumTargetAmount), - MinAllowedAmount: strconv.Itoa(arbitrumMinAmount), - MaxRebalancingGasCostUUSDC: "50", - ProfitabilityTimeout: 1 * time.Hour, - TransferCostCapUUSDC: "100", - }, - nil, - ) - - mockConfigReader.EXPECT().GetUSDCDenom(osmosisChainID).Return(osmosisUSDCDenom, nil) - mockConfigReader.EXPECT().GetUSDCDenom(arbitrumChainID).Return(arbitrumUSDCDenom, nil) - mockConfigReader.On("GetChainConfig", osmosisChainID).Return( - config.ChainConfig{ - Type: config.ChainType_COSMOS, - USDCDenom: osmosisUSDCDenom, - SolverAddress: osmosisAddress, - }, - nil, - ) - mockConfigReader.On("GetChainConfig", arbitrumChainID).Return( - config.ChainConfig{ - Type: config.ChainType_EVM, - USDCDenom: arbitrumUSDCDenom, - SolverAddress: arbitrumAddress, - }, - nil, - ) - ctx = config.ConfigReaderContext(ctx, mockConfigReader) - - f, err := loadKeysFile(defaultKeys) - assert.NoError(t, err) - - mockSkipGo := mock_skipgo.NewMockSkipGoClient(t) - mockEVMClientManager := mock_evmrpc.NewMockEVMRPCClientManager(t) - mockEVMClient := mock_evmrpc.NewMockEVMChainRPC(t) - mockEVMClientManager.EXPECT().GetClient(mockContext, arbitrumChainID).Return(mockEVMClient, nil) - - abi, err := usdc.UsdcMetaData.GetAbi() - assert.NoError(t, err) - data, err := abi.Pack("allowance", common.HexToAddress(arbitrumAddress), common.HexToAddress("0xskipgo")) - assert.NoError(t, err) - - to := common.HexToAddress(arbitrumUSDCDenom) - msg := ethereum.CallMsg{From: common.Address{}, To: &to, Data: data} - var nilBigInt *big.Int - mockEVMClient.EXPECT().CallContract(mock.Anything, msg, nilBigInt).Return(common.LeftPadBytes(big.NewInt(100).Bytes(), 32), nil) - - mockDatabse := mock_database.NewMockDatabase(t) - - pending := db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return(nil, nil) - - // basic initialization of a rebalance transfer - init := db.InitializeRebalanceTransferParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().InitializeRebalanceTransfer(mock.Anything, init).Return(0, nil) - - mockEVMTxExecutor := evm2.NewMockEVMTxExecutor(t) - mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, []byte{}, "999", osmosisAddress, mock.Anything).Return("arbitrum hash", nil) - - // mock executing the approval tx - mockEVMTxExecutor.On("ExecuteTx", mockContext, arbitrumChainID, arbitrumAddress, mock.Anything, "0", arbitrumUSDCDenom, mock.Anything).Return("arbitrum approval hash", nil) - - mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ - RebalanceTransferID: sql.NullInt64{Int64: 0, Valid: true}, - ChainID: arbitrumChainID, - TxHash: "arbitrum approval hash", - TxType: dbtypes.TxTypeERC20Approval, - TxStatus: dbtypes.TxStatusPending, - }).Return(db.SubmittedTx{}, nil).Twice() - - keystore, err := keys.LoadKeyStoreFromPlaintextFile(f.Name()) - assert.NoError(t, err) - - mockTxPriceOracle := mock_oracle.NewMockTxPriceOracle(t) - mockEVMClient.EXPECT().SuggestGasPrice(mockContext).Return(big.NewInt(1000000000), nil) - mockTxPriceOracle.On("TxFeeUUSDC", mockContext, mock.Anything).Return(big.NewInt(75), nil) - - rebalancer, err := NewFundRebalancer(ctx, keystore, mockSkipGo, mockEVMClientManager, mockDatabse, mockTxPriceOracle, mockEVMTxExecutor) - assert.NoError(t, err) - - // setup initial state of mocks - - // no pending txns - mockDatabse.EXPECT().GetAllPendingRebalanceTransfers(mockContext).Return(nil, nil).Maybe() - mockDatabse.EXPECT().GetPendingRebalanceTransfersToChain(mockContext, osmosisChainID).Return(nil, nil) - - // osmosis balance lower than min amount, arbitrum & eth balances higher than target - mockSkipGo.EXPECT().Balance(mockContext, osmosisChainID, osmosisAddress, osmosisUSDCDenom).Return("0", nil) - mockEVMClient.EXPECT().GetUSDCBalance(mockContext, arbitrumUSDCDenom, arbitrumAddress).Return(big.NewInt(1000), nil) - - route := &skipgo.RouteResponse{ - AmountOut: strconv.Itoa(osmosisTargetAmount), - Operations: []any{"opts"}, - RequiredChainAddresses: []string{arbitrumChainID, osmosisChainID}, - } - mockSkipGo.EXPECT().Route(mockContext, arbitrumUSDCDenom, arbitrumChainID, osmosisUSDCDenom, osmosisChainID, big.NewInt(osmosisTargetAmount)). - Return(route, nil).Twice() - - txs := []skipgo.Tx{{ - EVMTx: &skipgo.EVMTx{ - ChainID: arbitrumChainID, - To: osmosisAddress, - Value: "999", - RequiredERC20Approvals: []skipgo.ERC20Approval{{ - TokenContract: arbitrumUSDCDenom, - Spender: "0xskipgo", - Amount: "999", - }}, - SignerAddress: arbitrumAddress, - }}} - mockSkipGo.EXPECT().Msgs(mockContext, arbitrumUSDCDenom, arbitrumChainID, arbitrumAddress, osmosisUSDCDenom, osmosisChainID, osmosisAddress, big.NewInt(osmosisTargetAmount), big.NewInt(osmosisTargetAmount), []string{arbitrumAddress, osmosisAddress}, route.Operations). - Return(txs, nil).Twice() - - mockEVMClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100), nil) - - // this call to rebalance will see that the gas is not acceptable and not insert the rebalance tx - rebalancer.Rebalance(ctx) - - update := db.UpdateTransferParams{TxHash: "arbitrum hash", Amount: strconv.Itoa(osmosisTargetAmount), ID: 0} - mockDatabse.EXPECT().UpdateTransfer(mockContext, update).Return(nil) + mockDatabse.EXPECT().InsertRebalanceTransfer(mockContext, db.InsertRebalanceTransferParams{ + TxHash: "arbitrum hash", + SourceChainID: arbitrumChainID, + DestinationChainID: osmosisChainID, + Amount: strconv.Itoa(osmosisTargetAmount), + }).Return(0, nil) // insert tx into submitted txs table mockDatabse.EXPECT().InsertSubmittedTx(mockContext, db.InsertSubmittedTxParams{ @@ -1037,21 +867,7 @@ func TestFundRebalancer_Rebalance(t *testing.T) { TxStatus: dbtypes.TxStatusPending, }).Return(db.SubmittedTx{}, nil).Once() - // modify the failure so that it is now timed out and uses the timeout - // cost cap - rebalancer.profitabilityFailures[arbitrumChainID].firstFailureTime = time.Now().Add(-2 * time.Hour) - - // now return the previous rebalance transfer we inserted - pending = db.GetPendingRebalanceTransfersBetweenChainsParams{SourceChainID: arbitrumChainID, DestinationChainID: osmosisChainID} - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Unset() - mockDatabse.EXPECT().GetPendingRebalanceTransfersBetweenChains(mock.Anything, pending).Return([]db.GetPendingRebalanceTransfersBetweenChainsRow{{ - ID: 0, - SourceChainID: arbitrumChainID, - DestinationChainID: osmosisChainID, - }}, nil) - rebalancer.Rebalance(ctx) - }) } diff --git a/mocks/fundrebalancer/fake_database.go b/mocks/fundrebalancer/fake_database.go index 9335d04..f1adccf 100644 --- a/mocks/fundrebalancer/fake_database.go +++ b/mocks/fundrebalancer/fake_database.go @@ -54,34 +54,6 @@ func (fdb *FakeDatabase) InsertSubmittedTx(ctx context.Context, arg db.InsertSub return db.SubmittedTx{}, nil } -func (fdb *FakeDatabase) InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) { - return fdb.InsertRebalanceTransfer(ctx, db.InsertRebalanceTransferParams{ - TxHash: "", - SourceChainID: arg.SourceChainID, - DestinationChainID: arg.DestinationChainID, - Amount: "0", - }) -} - -func (fdb *FakeDatabase) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) { - fdb.dbLock.RLock() - defer fdb.dbLock.RUnlock() - - var pendingTransfers []db.GetPendingRebalanceTransfersBetweenChainsRow - for _, transfer := range fdb.db { - if transfer.Status == "PENDING" && transfer.DestinationChainID == arg.DestinationChainID && transfer.SourceChainID == arg.SourceChainID { - pendingTransfers = append(pendingTransfers, db.GetPendingRebalanceTransfersBetweenChainsRow{ - ID: transfer.ID, - TxHash: transfer.TxHash, - SourceChainID: transfer.SourceChainID, - DestinationChainID: transfer.DestinationChainID, - Amount: transfer.Amount, - }) - } - } - return pendingTransfers, nil -} - func (fdb *FakeDatabase) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) { fdb.dbLock.Lock() defer fdb.dbLock.Unlock() @@ -124,20 +96,6 @@ func (fdb *FakeDatabase) GetAllPendingRebalanceTransfers(ctx context.Context) ([ return pendingTransfers, nil } -func (fdb *FakeDatabase) UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error { - fdb.dbLock.Lock() - defer fdb.dbLock.Unlock() - - for _, transfer := range fdb.db { - if transfer.ID == arg.ID { - transfer.TxHash = arg.TxHash - transfer.Amount = arg.Amount - } - } - - return nil -} - func (fdb *FakeDatabase) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error { fdb.dbLock.Lock() defer fdb.dbLock.Unlock() diff --git a/mocks/fundrebalancer/mock_database.go b/mocks/fundrebalancer/mock_database.go index cb8546b..68ff221 100644 --- a/mocks/fundrebalancer/mock_database.go +++ b/mocks/fundrebalancer/mock_database.go @@ -80,65 +80,6 @@ func (_c *MockDatabase_GetAllPendingRebalanceTransfers_Call) RunAndReturn(run fu return _c } -// GetPendingRebalanceTransfersBetweenChains provides a mock function with given fields: ctx, arg -func (_m *MockDatabase) GetPendingRebalanceTransfersBetweenChains(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error) { - ret := _m.Called(ctx, arg) - - if len(ret) == 0 { - panic("no return value specified for GetPendingRebalanceTransfersBetweenChains") - } - - var r0 []db.GetPendingRebalanceTransfersBetweenChainsRow - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error)); ok { - return rf(ctx, arg) - } - if rf, ok := ret.Get(0).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) []db.GetPendingRebalanceTransfersBetweenChainsRow); ok { - r0 = rf(ctx, arg) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]db.GetPendingRebalanceTransfersBetweenChainsRow) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) error); ok { - r1 = rf(ctx, arg) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPendingRebalanceTransfersBetweenChains' -type MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call struct { - *mock.Call -} - -// GetPendingRebalanceTransfersBetweenChains is a helper method to define mock.On call -// - ctx context.Context -// - arg db.GetPendingRebalanceTransfersBetweenChainsParams -func (_e *MockDatabase_Expecter) GetPendingRebalanceTransfersBetweenChains(ctx interface{}, arg interface{}) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { - return &MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call{Call: _e.mock.On("GetPendingRebalanceTransfersBetweenChains", ctx, arg)} -} - -func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) Run(run func(ctx context.Context, arg db.GetPendingRebalanceTransfersBetweenChainsParams)) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(db.GetPendingRebalanceTransfersBetweenChainsParams)) - }) - return _c -} - -func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) Return(_a0 []db.GetPendingRebalanceTransfersBetweenChainsRow, _a1 error) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call) RunAndReturn(run func(context.Context, db.GetPendingRebalanceTransfersBetweenChainsParams) ([]db.GetPendingRebalanceTransfersBetweenChainsRow, error)) *MockDatabase_GetPendingRebalanceTransfersBetweenChains_Call { - _c.Call.Return(run) - return _c -} - // GetPendingRebalanceTransfersToChain provides a mock function with given fields: ctx, destinationChainID func (_m *MockDatabase) GetPendingRebalanceTransfersToChain(ctx context.Context, destinationChainID string) ([]db.GetPendingRebalanceTransfersToChainRow, error) { ret := _m.Called(ctx, destinationChainID) @@ -198,63 +139,6 @@ func (_c *MockDatabase_GetPendingRebalanceTransfersToChain_Call) RunAndReturn(ru return _c } -// InitializeRebalanceTransfer provides a mock function with given fields: ctx, arg -func (_m *MockDatabase) InitializeRebalanceTransfer(ctx context.Context, arg db.InitializeRebalanceTransferParams) (int64, error) { - ret := _m.Called(ctx, arg) - - if len(ret) == 0 { - panic("no return value specified for InitializeRebalanceTransfer") - } - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, db.InitializeRebalanceTransferParams) (int64, error)); ok { - return rf(ctx, arg) - } - if rf, ok := ret.Get(0).(func(context.Context, db.InitializeRebalanceTransferParams) int64); ok { - r0 = rf(ctx, arg) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, db.InitializeRebalanceTransferParams) error); ok { - r1 = rf(ctx, arg) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDatabase_InitializeRebalanceTransfer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitializeRebalanceTransfer' -type MockDatabase_InitializeRebalanceTransfer_Call struct { - *mock.Call -} - -// InitializeRebalanceTransfer is a helper method to define mock.On call -// - ctx context.Context -// - arg db.InitializeRebalanceTransferParams -func (_e *MockDatabase_Expecter) InitializeRebalanceTransfer(ctx interface{}, arg interface{}) *MockDatabase_InitializeRebalanceTransfer_Call { - return &MockDatabase_InitializeRebalanceTransfer_Call{Call: _e.mock.On("InitializeRebalanceTransfer", ctx, arg)} -} - -func (_c *MockDatabase_InitializeRebalanceTransfer_Call) Run(run func(ctx context.Context, arg db.InitializeRebalanceTransferParams)) *MockDatabase_InitializeRebalanceTransfer_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(db.InitializeRebalanceTransferParams)) - }) - return _c -} - -func (_c *MockDatabase_InitializeRebalanceTransfer_Call) Return(_a0 int64, _a1 error) *MockDatabase_InitializeRebalanceTransfer_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDatabase_InitializeRebalanceTransfer_Call) RunAndReturn(run func(context.Context, db.InitializeRebalanceTransferParams) (int64, error)) *MockDatabase_InitializeRebalanceTransfer_Call { - _c.Call.Return(run) - return _c -} - // InsertRebalanceTransfer provides a mock function with given fields: ctx, arg func (_m *MockDatabase) InsertRebalanceTransfer(ctx context.Context, arg db.InsertRebalanceTransferParams) (int64, error) { ret := _m.Called(ctx, arg) @@ -369,53 +253,6 @@ func (_c *MockDatabase_InsertSubmittedTx_Call) RunAndReturn(run func(context.Con return _c } -// UpdateTransfer provides a mock function with given fields: ctx, arg -func (_m *MockDatabase) UpdateTransfer(ctx context.Context, arg db.UpdateTransferParams) error { - ret := _m.Called(ctx, arg) - - if len(ret) == 0 { - panic("no return value specified for UpdateTransfer") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, db.UpdateTransferParams) error); ok { - r0 = rf(ctx, arg) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockDatabase_UpdateTransfer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTransfer' -type MockDatabase_UpdateTransfer_Call struct { - *mock.Call -} - -// UpdateTransfer is a helper method to define mock.On call -// - ctx context.Context -// - arg db.UpdateTransferParams -func (_e *MockDatabase_Expecter) UpdateTransfer(ctx interface{}, arg interface{}) *MockDatabase_UpdateTransfer_Call { - return &MockDatabase_UpdateTransfer_Call{Call: _e.mock.On("UpdateTransfer", ctx, arg)} -} - -func (_c *MockDatabase_UpdateTransfer_Call) Run(run func(ctx context.Context, arg db.UpdateTransferParams)) *MockDatabase_UpdateTransfer_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(db.UpdateTransferParams)) - }) - return _c -} - -func (_c *MockDatabase_UpdateTransfer_Call) Return(_a0 error) *MockDatabase_UpdateTransfer_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockDatabase_UpdateTransfer_Call) RunAndReturn(run func(context.Context, db.UpdateTransferParams) error) *MockDatabase_UpdateTransfer_Call { - _c.Call.Return(run) - return _c -} - // UpdateTransferStatus provides a mock function with given fields: ctx, arg func (_m *MockDatabase) UpdateTransferStatus(ctx context.Context, arg db.UpdateTransferStatusParams) error { ret := _m.Called(ctx, arg) From aa840811d263d9c94df6e8120f2ce030dd153a87 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 5 Dec 2024 15:18:02 -0500 Subject: [PATCH 17/20] change fee parsing to decode tx auth info bytes and return gas denom fee --- shared/bridges/cctp/cosmos_bridge_client.go | 33 ++++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/shared/bridges/cctp/cosmos_bridge_client.go b/shared/bridges/cctp/cosmos_bridge_client.go index 03fc3c0..ff6c859 100644 --- a/shared/bridges/cctp/cosmos_bridge_client.go +++ b/shared/bridges/cctp/cosmos_bridge_client.go @@ -173,35 +173,26 @@ func (c *CosmosBridgeClient) GetTxResult(ctx context.Context, txHash string) (*b return nil, nil, err } + tx, err := c.txConfig.TxDecoder()(result.Tx) + if err != nil { + return nil, nil, fmt.Errorf("decoding tx bytes: %w", err) + } + // parse tx fee event and use as the gas cost. we are using the fee event // as the gas cost, technically this is not always true for all cosmos // txns, however for all of the transactions that the solver will submit // and get results of via this function, this should be true - var fee sdk.Coin -outer: - for _, event := range result.TxResult.GetEvents() { - if event.GetType() != "tx" { - continue - } - - for _, attribute := range event.GetAttributes() { - if attribute.GetKey() != "fee" { - continue - } - - coin, err := sdk.ParseCoinNormalized(attribute.GetValue()) - if err != nil { - return nil, nil, fmt.Errorf("parsing coin from tx fee event attribute %s: %w", attribute.GetValue(), err) - } - fee = coin - break outer - } + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return nil, nil, fmt.Errorf("could not convert decoded tx to sdk.FeeTx") } + fee := feeTx.GetFee().AmountOf(c.gasDenom) + if result.TxResult.Code != 0 { - return fee.Amount.BigInt(), &TxFailure{fmt.Sprintf("tx failed with code: %d and log: %s", result.TxResult.Code, result.TxResult.Log)}, nil + return fee.BigInt(), &TxFailure{fmt.Sprintf("tx failed with code: %d and log: %s", result.TxResult.Code, result.TxResult.Log)}, nil } - return fee.Amount.BigInt(), nil, nil + return fee.BigInt(), nil, nil } func (c *CosmosBridgeClient) IsSettlementComplete(ctx context.Context, gatewayContractAddress, orderID string) (bool, error) { From a77316f30a1487e70408741a013d2b6795d055f7 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Thu, 5 Dec 2024 15:20:36 -0500 Subject: [PATCH 18/20] return an error if fee for gas denom is not found in cosmos tx result --- shared/bridges/cctp/cosmos_bridge_client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared/bridges/cctp/cosmos_bridge_client.go b/shared/bridges/cctp/cosmos_bridge_client.go index ff6c859..2021ed7 100644 --- a/shared/bridges/cctp/cosmos_bridge_client.go +++ b/shared/bridges/cctp/cosmos_bridge_client.go @@ -188,6 +188,9 @@ func (c *CosmosBridgeClient) GetTxResult(ctx context.Context, txHash string) (*b } fee := feeTx.GetFee().AmountOf(c.gasDenom) + if fee == math.ZeroInt() { + return nil, nil, fmt.Errorf("zero fee amount found for denom %s in tx result auth info", c.gasDenom) + } if result.TxResult.Code != 0 { return fee.BigInt(), &TxFailure{fmt.Sprintf("tx failed with code: %d and log: %s", result.TxResult.Code, result.TxResult.Log)}, nil From b4f08e54767ab9a590f9ed4ef5736f8cb712b65c Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Fri, 6 Dec 2024 12:46:50 -0500 Subject: [PATCH 19/20] fix db migration naming and make rebalance_transfer_id a foreign key --- ...00008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql | 1 - ... => 000008_add_tx_price_cost_column_to_submitted_tx.down.sql} | 0 ...ql => 000008_add_tx_price_cost_column_to_submitted_tx.up.sql} | 0 ...09_add_rebalance_transfer_id_column_to_submitted_tx.down.sql} | 0 ...00009_add_rebalance_transfer_id_column_to_submitted_tx.up.sql | 1 + 5 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql rename db/migrations/{000007_add_tx_price_cost_column_to_submitted_tx.down.sql => 000008_add_tx_price_cost_column_to_submitted_tx.down.sql} (100%) rename db/migrations/{000007_add_tx_price_cost_column_to_submitted_tx.up.sql => 000008_add_tx_price_cost_column_to_submitted_tx.up.sql} (100%) rename db/migrations/{000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql => 000009_add_rebalance_transfer_id_column_to_submitted_tx.down.sql} (100%) create mode 100644 db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.up.sql diff --git a/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql b/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql deleted file mode 100644 index 2a90318..0000000 --- a/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE submitted_txs ADD rebalance_transfer_id INT; diff --git a/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql b/db/migrations/000008_add_tx_price_cost_column_to_submitted_tx.down.sql similarity index 100% rename from db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.down.sql rename to db/migrations/000008_add_tx_price_cost_column_to_submitted_tx.down.sql diff --git a/db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql b/db/migrations/000008_add_tx_price_cost_column_to_submitted_tx.up.sql similarity index 100% rename from db/migrations/000007_add_tx_price_cost_column_to_submitted_tx.up.sql rename to db/migrations/000008_add_tx_price_cost_column_to_submitted_tx.up.sql diff --git a/db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql b/db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.down.sql similarity index 100% rename from db/migrations/000008_add_rebalance_transfer_id_column_to_submitted_tx.down.sql rename to db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.down.sql diff --git a/db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.up.sql b/db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.up.sql new file mode 100644 index 0000000..cb9104d --- /dev/null +++ b/db/migrations/000009_add_rebalance_transfer_id_column_to_submitted_tx.up.sql @@ -0,0 +1 @@ +ALTER TABLE submitted_txs ADD COLUMN rebalance_transfer_id INT REFERENCES rebalance_transfers(id); From 540883429e6d2e1d1c9a1bf6fe95535a44adeea3 Mon Sep 17 00:00:00 2001 From: Matt Acciai Date: Fri, 6 Dec 2024 12:47:26 -0500 Subject: [PATCH 20/20] pass chainid to tx price oracle --- fundrebalancer/fundrebalancer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fundrebalancer/fundrebalancer.go b/fundrebalancer/fundrebalancer.go index d3e8a9e..708ba3b 100644 --- a/fundrebalancer/fundrebalancer.go +++ b/fundrebalancer/fundrebalancer.go @@ -775,9 +775,15 @@ func (r *FundRebalancer) isGasAcceptable(ctx context.Context, txn SkipGoTxnWithM return false, "", fmt.Errorf("getting chain fund rebalancing config: %w", err) } + chainIDBigInt, ok := new(big.Int).SetString(chainID, 10) + if !ok { + return false, "", fmt.Errorf("could not convert chainID %s to *big.Int", chainID) + } + gasCostUUSDC, err := r.txPriceOracle.TxFeeUUSDC(ctx, types.NewTx(&types.DynamicFeeTx{ Gas: txn.gasEstimate, GasFeeCap: gasPrice, + ChainID: chainIDBigInt, })) if err != nil { return false, "", fmt.Errorf("calculating total fund rebalancing gas cost in UUSDC: %w", err)