diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 208f94a409..d4bef685ec 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -336,7 +336,7 @@ var AllE2ETests = []runner.E2ETest{ runner.NewE2ETest( TestBitcoinDepositRefundName, "deposit Bitcoin into ZEVM; expect refund", []runner.ArgDefinition{ - {Description: "amount in btc", DefaultValue: "0.001"}, + {Description: "amount in btc", DefaultValue: "0.1"}, }, TestBitcoinDepositRefund, ), diff --git a/e2e/e2etests/test_bitcoin_deposit_refund.go b/e2e/e2etests/test_bitcoin_deposit_refund.go index 141ef33697..79a404a9ff 100644 --- a/e2e/e2etests/test_bitcoin_deposit_refund.go +++ b/e2e/e2etests/test_bitcoin_deposit_refund.go @@ -1,12 +1,15 @@ package e2etests import ( + "context" "strconv" + "time" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/runner" "github.com/zeta-chain/zetacore/e2e/utils" "github.com/zeta-chain/zetacore/x/crosschain/types" + zetabitcoin "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" ) func TestBitcoinDepositRefund(r *runner.E2ERunner, args []string) { @@ -14,28 +17,54 @@ func TestBitcoinDepositRefund(r *runner.E2ERunner, args []string) { // Given amount to send require.Len(r, args, 1) amount := parseFloat(r, args[0]) + amount += zetabitcoin.DefaultDepositorFee // Given BTC address r.SetBtcAddress(r.Name, false) // Given a list of UTXOs - utxos, err := r.BtcRPCClient.ListUnspent() + utxos, err := r.ListDeployerUTXOs() require.NoError(r, err) require.NotEmpty(r, utxos) // ACT - // Send a single UTXO to TSS address + // Send BTC to TSS address with a dummy memo txHash, err := r.SendToTSSFromDeployerWithMemo(amount, utxos, []byte("gibberish-memo")) require.NoError(r, err) + require.NotEmpty(r, txHash) // Wait for processing in zetaclient - cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + cctxs := utils.WaitCctxByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) // ASSERT + require.Len(r, cctxs, 1) + // Check that it's status is related to tx reversal - actualStatus := cctx.CctxStatus.Status + expectedStatuses := []string{types.CctxStatus_PendingRevert.String(), types.CctxStatus_Reverted.String()} + actualStatus := cctxs[0].CctxStatus.Status.String() + + require.Contains(r, expectedStatuses, actualStatus) + + r.Logger.Info("CCTX revert status: %s", actualStatus) + + // Now we want to make sure refund TX is completed. Let's check that zetaclient issued a refund on BTC + ctx, cancel := context.WithTimeout(r.Ctx, time.Minute*10) + defer cancel() + + searchForCrossChainWithBtcRefund := utils.Matches(func(tx types.CrossChainTx) bool { + if len(tx.OutboundParams) != 2 { + return false + } + + btcRefundTx := tx.OutboundParams[1] + + return btcRefundTx.Hash != "" + }) + + cctxs = utils.WaitCctxByInboundHash(ctx, r, txHash.String(), r.CctxClient, searchForCrossChainWithBtcRefund) + require.Len(r, cctxs, 1) - require.Contains(r, []types.CctxStatus{types.CctxStatus_PendingRevert, types.CctxStatus_Reverted}, actualStatus) + // todo check that BTC refund is completed } func parseFloat(t require.TestingT, s string) float64 { diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index 093ff4314f..0504dc1481 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -7,10 +7,14 @@ import ( rpchttp "github.com/cometbft/cometbft/rpc/client/http" coretypes "github.com/cometbft/cometbft/rpc/core/types" - + "github.com/stretchr/testify/require" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) +type CCTXClient = crosschaintypes.QueryClient + const ( FungibleAdminName = "fungibleadmin" @@ -170,6 +174,73 @@ func WaitCCTXMinedByIndex( } } +type WaitOpts func(c *waitConfig) + +// MatchStatus waits for a specific CCTX status. +func MatchStatus(s crosschaintypes.CctxStatus) WaitOpts { + return Matches(func(tx crosschaintypes.CrossChainTx) bool { + return tx.CctxStatus != nil && tx.CctxStatus.Status == s + }) +} + +func Matches(fn func(tx crosschaintypes.CrossChainTx) bool) WaitOpts { + return func(c *waitConfig) { c.matchFunction = fn } +} + +type waitConfig struct { + matchFunction func(tx crosschaintypes.CrossChainTx) bool +} + +// WaitCctxByInboundHash waits until cctx appears by inbound hash. +func WaitCctxByInboundHash(ctx context.Context, t require.TestingT, hash string, c CCTXClient, opts ...WaitOpts) []crosschaintypes.CrossChainTx { + const tick = time.Millisecond * 200 + + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, DefaultCctxTimeout) + defer cancel() + } + + in := &crosschaintypes.QueryInboundHashToCctxDataRequest{InboundHash: hash} + + var cfg waitConfig + for _, opt := range opts { + opt(&cfg) + } + + matches := func(txs []crosschaintypes.CrossChainTx) bool { + if cfg.matchFunction == nil { + return true + } + + for _, tx := range txs { + if ok := cfg.matchFunction(tx); !ok { + return false + } + } + + return true + } + + for { + out, err := c.InTxHashToCctxData(ctx, in) + statusCode, _ := status.FromError(err) + + switch { + case statusCode.Code() == codes.NotFound: + // expected; let's retry + case err != nil: + require.NoError(t, err, "failed to get cctx by inbound hash: %s", hash) + case len(out.CrossChainTxs) > 0 && matches(out.CrossChainTxs): + return out.CrossChainTxs + case ctx.Err() == nil: + require.NoError(t, err, "failed to get cctx by inbound hash (ctx error): %s", hash) + } + + time.Sleep(tick) + } +} + func IsTerminalStatus(status crosschaintypes.CctxStatus) bool { return status == crosschaintypes.CctxStatus_OutboundMined || status == crosschaintypes.CctxStatus_Aborted ||