github.com/0xsequence/ethkit@v1.25.0/ethreceipts/ethreceipts_test.go (about)

     1  package ethreceipts_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/big"
     8  	"sync"
     9  	"sync/atomic"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/0xsequence/ethkit"
    14  	"github.com/0xsequence/ethkit/ethmonitor"
    15  	"github.com/0xsequence/ethkit/ethreceipts"
    16  	"github.com/0xsequence/ethkit/ethtest"
    17  	"github.com/0xsequence/ethkit/ethtxn"
    18  	"github.com/0xsequence/ethkit/go-ethereum/common"
    19  	"github.com/0xsequence/ethkit/go-ethereum/core/types"
    20  	"github.com/goware/logger"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  var (
    26  	testchain *ethtest.Testchain
    27  	log       logger.Logger
    28  )
    29  
    30  func init() {
    31  	var err error
    32  	testchain, err = ethtest.NewTestchain()
    33  	if err != nil {
    34  		panic(err)
    35  	}
    36  
    37  	// log = logger.NewLogger(logger.LogLevel_INFO)
    38  	log = logger.NewLogger(logger.LogLevel_DEBUG)
    39  }
    40  
    41  // Test fetching the chain id to ensure we can connect to the testchain properly
    42  func TestTestchainID(t *testing.T) {
    43  	assert.Equal(t, testchain.ChainID().Uint64(), uint64(1337))
    44  }
    45  
    46  func TestFetchTransactionReceiptBasic(t *testing.T) {
    47  	ctx, cancel := context.WithCancel(context.Background())
    48  	defer cancel()
    49  
    50  	//
    51  	// Setup ReceiptsListener
    52  	//
    53  	provider := testchain.Provider
    54  
    55  	monitorOptions := ethmonitor.DefaultOptions
    56  	// monitorOptions.Logger = log
    57  	monitorOptions.WithLogs = true
    58  	monitorOptions.BlockRetentionLimit = 1000
    59  
    60  	monitor, err := ethmonitor.NewMonitor(provider, monitorOptions)
    61  	assert.NoError(t, err)
    62  
    63  	go func() {
    64  		err := monitor.Run(ctx)
    65  		if err != nil {
    66  			t.Error(err)
    67  		}
    68  	}()
    69  
    70  	require.Zero(t, monitor.NumSubscribers())
    71  
    72  	listenerOptions := ethreceipts.DefaultOptions
    73  	listenerOptions.NumBlocksToFinality = 10
    74  	listenerOptions.FilterMaxWaitNumBlocks = 4
    75  
    76  	receiptsListener, err := ethreceipts.NewReceiptsListener(log, provider, monitor, listenerOptions)
    77  	assert.NoError(t, err)
    78  
    79  	go func() {
    80  		err := receiptsListener.Run(ctx)
    81  		if err != nil {
    82  			t.Error(err)
    83  		}
    84  	}()
    85  
    86  	//
    87  	// Setup test wallet
    88  	//
    89  	wallet, _ := testchain.DummyWallet(1)
    90  	testchain.MustFundAddress(wallet.Address())
    91  
    92  	// numTxns := 1
    93  	// numTxns := 2
    94  	// numTxns := 10
    95  	numTxns := 40
    96  	lastNonce, err := wallet.GetNonce(ctx)
    97  	require.NoError(t, err)
    98  	wallet2, _ := testchain.DummyWallet(2)
    99  
   100  	txns := []*types.Transaction{}
   101  	txnHashes := []common.Hash{}
   102  
   103  	for i := 0; i < numTxns; i++ {
   104  		to := wallet2.Address()
   105  		txr := &ethtxn.TransactionRequest{
   106  			To:       &to,
   107  			ETHValue: ethtest.ETHValue(0.1),
   108  			GasLimit: 120_000,
   109  			Nonce:    big.NewInt(int64(lastNonce + uint64(i))),
   110  		}
   111  
   112  		txn, err := wallet.NewTransaction(ctx, txr)
   113  		require.NoError(t, err)
   114  
   115  		txns = append(txns, txn)
   116  		txnHashes = append(txnHashes, txn.Hash())
   117  	}
   118  
   119  	// dispatch txns in the background
   120  	go func() {
   121  		for _, txn := range txns {
   122  			_, _, err = wallet.SendTransaction(ctx, txn)
   123  			require.NoError(t, err)
   124  			// time.Sleep(500 * time.Millisecond)
   125  		}
   126  	}()
   127  
   128  	// ensure all txns made it
   129  	// delay processing if we want to make sure SearchCache works
   130  	// time.Sleep(2 * time.Second)
   131  	// for _, txnHash := range txnHashes {
   132  	// 	receipt, err := provider.TransactionReceipt(context.Background(), txnHash)
   133  	// 	require.NoError(t, err)
   134  	// 	require.True(t, receipt.Status == 1)
   135  	// }
   136  
   137  	// Let's listen for all the txns
   138  	var wg sync.WaitGroup
   139  	for i, txnHash := range txnHashes {
   140  		wg.Add(1)
   141  		go func(i int, txnHash common.Hash) {
   142  			defer wg.Done()
   143  
   144  			receipt, waitFinality, err := receiptsListener.FetchTransactionReceipt(ctx, txnHash, 7)
   145  			require.NoError(t, err)
   146  			require.NotNil(t, receipt)
   147  			require.True(t, receipt.Status() == types.ReceiptStatusSuccessful)
   148  			require.False(t, receipt.Final)
   149  
   150  			t.Logf("=> MINED %d :: %s", i, receipt.TransactionHash().String())
   151  
   152  			_ = waitFinality
   153  			finalReceipt, err := waitFinality(context.Background())
   154  			require.NoError(t, err)
   155  			require.NotNil(t, finalReceipt)
   156  			require.True(t, finalReceipt.Status() == types.ReceiptStatusSuccessful)
   157  			require.True(t, finalReceipt.Final)
   158  
   159  			t.Logf("=> FINAL %d :: %s", i, receipt.TransactionHash().String())
   160  		}(i, txnHash)
   161  	}
   162  	wg.Wait()
   163  
   164  	time.Sleep(2 * time.Second)
   165  
   166  	// Check subscribers
   167  	require.Zero(t, receiptsListener.NumSubscribers())
   168  	require.Equal(t, 1, monitor.NumSubscribers())
   169  
   170  	// Testing exhausted filter after maxWait period is unable to find non-existant txn hash
   171  	receipt, waitFinality, err := receiptsListener.FetchTransactionReceipt(ctx, ethkit.Hash{1, 2, 3, 4}, 5)
   172  	require.Error(t, err)
   173  	require.True(t, errors.Is(err, ethreceipts.ErrFilterExhausted))
   174  	require.Nil(t, receipt)
   175  	finalReceipt, err := waitFinality(context.Background())
   176  	require.Error(t, err)
   177  	require.True(t, errors.Is(err, ethreceipts.ErrFilterExhausted), "received error %v", err)
   178  	require.Nil(t, finalReceipt)
   179  
   180  	// Check subscribers
   181  	time.Sleep(1 * time.Second)
   182  	require.Zero(t, receiptsListener.NumSubscribers())
   183  	require.Equal(t, 1, monitor.NumSubscribers())
   184  
   185  	// Clear monitor retention, and lets try to find an old txnHash which is on the chain
   186  	// and will force to use SearchOnChain method.
   187  	monitor.PurgeHistory()
   188  	receiptsListener.PurgeHistory()
   189  
   190  	receipt, waitFinality, err = receiptsListener.FetchTransactionReceipt(ctx, txnHashes[0])
   191  	require.NoError(t, err)
   192  	require.NotNil(t, receipt)
   193  	finalReceipt, err = waitFinality(context.Background())
   194  	require.NoError(t, err)
   195  	require.NotNil(t, finalReceipt)
   196  	require.True(t, finalReceipt.Final)
   197  
   198  	// wait enough time, so that the fetched receipt will come as finalized right away
   199  	time.Sleep(5 * time.Second)
   200  
   201  	receipt, waitFinality, err = receiptsListener.FetchTransactionReceipt(ctx, txnHashes[1])
   202  	require.NoError(t, err)
   203  	require.NotNil(t, receipt)
   204  	require.True(t, receipt.Final)
   205  	finalReceipt, err = waitFinality(context.Background())
   206  	require.NoError(t, err)
   207  	require.NotNil(t, finalReceipt)
   208  	require.True(t, finalReceipt.Final)
   209  
   210  	// Check subscribers
   211  	time.Sleep(1 * time.Second)
   212  	require.Zero(t, receiptsListener.NumSubscribers())
   213  	require.Equal(t, 1, monitor.NumSubscribers())
   214  }
   215  
   216  func TestFetchTransactionReceiptBlast(t *testing.T) {
   217  	ctx, cancel := context.WithCancel(context.Background())
   218  	defer cancel()
   219  
   220  	//
   221  	// Setup ReceiptsListener
   222  	//
   223  	provider := testchain.Provider
   224  
   225  	monitorOptions := ethmonitor.DefaultOptions
   226  	// monitorOptions.Logger = log
   227  	monitorOptions.WithLogs = true
   228  	monitorOptions.BlockRetentionLimit = 1000
   229  
   230  	monitor, err := ethmonitor.NewMonitor(provider, monitorOptions)
   231  	assert.NoError(t, err)
   232  
   233  	go func() {
   234  		err := monitor.Run(ctx)
   235  		if err != nil {
   236  			t.Error(err)
   237  		}
   238  	}()
   239  
   240  	listenerOptions := ethreceipts.DefaultOptions
   241  	listenerOptions.NumBlocksToFinality = 10
   242  	listenerOptions.FilterMaxWaitNumBlocks = 4
   243  
   244  	receiptsListener, err := ethreceipts.NewReceiptsListener(log, provider, monitor, listenerOptions)
   245  	assert.NoError(t, err)
   246  
   247  	go func() {
   248  		err := receiptsListener.Run(ctx)
   249  		if err != nil {
   250  			t.Error(err)
   251  		}
   252  	}()
   253  
   254  	//
   255  	// Setup wallets
   256  	//
   257  
   258  	// create and fund a few wallets to send from
   259  	fromWallets, _ := testchain.DummyWallets(5, 100)
   260  	testchain.FundAddresses(ethtest.WalletAddresses(fromWallets), 10)
   261  
   262  	// create a few wallets to send to
   263  	toWallets, _ := testchain.DummyWallets(3, 200)
   264  
   265  	// prepare and sign bunch of txns
   266  	values := []*big.Int{}
   267  	for range fromWallets {
   268  		values = append(values, ethtest.ETHValue(0.1))
   269  	}
   270  
   271  	_, txns, err := ethtest.PrepareBlastSendTransactions(ctx, fromWallets, ethtest.WalletAddresses(toWallets), values)
   272  	assert.NoError(t, err)
   273  
   274  	// send the txns -- these will be async, so we can just blast synchronously
   275  	// and not have to do it in a goroutine
   276  	for _, txn := range txns {
   277  		_, _, err := ethtxn.SendTransaction(ctx, provider, txn)
   278  		assert.NoError(t, err)
   279  	}
   280  
   281  	// lets use receipt listener to listen on txns from just one of the wallets
   282  	txnHashes := []common.Hash{
   283  		txns[5].Hash(), txns[2].Hash(), txns[8].Hash(), txns[3].Hash(),
   284  	}
   285  
   286  	var count uint64
   287  
   288  	var wg sync.WaitGroup
   289  	for i, txnHash := range txnHashes {
   290  		wg.Add(1)
   291  		go func(i int, txnHash common.Hash) {
   292  			defer wg.Done()
   293  
   294  			receipt, receiptFinality, err := receiptsListener.FetchTransactionReceipt(ctx, txnHash)
   295  			assert.NoError(t, err)
   296  			assert.NotNil(t, receipt)
   297  			assert.True(t, receipt.Status() == types.ReceiptStatusSuccessful)
   298  
   299  			finalReceipt, err := receiptFinality(context.Background())
   300  			require.NoError(t, err)
   301  			require.True(t, finalReceipt.Status() == types.ReceiptStatusSuccessful)
   302  
   303  			t.Logf("=> %d :: %s", i, receipt.TransactionHash().String())
   304  
   305  			atomic.AddUint64(&count, 1)
   306  		}(i, txnHash)
   307  	}
   308  	wg.Wait()
   309  
   310  	require.Equal(t, int(count), len(txnHashes))
   311  }
   312  
   313  func TestReceiptsListenerFilters(t *testing.T) {
   314  	ctx, cancel := context.WithCancel(context.Background())
   315  	defer cancel()
   316  
   317  	//
   318  	// Setup ReceiptsListener
   319  	//
   320  	provider := testchain.Provider
   321  
   322  	monitorOptions := ethmonitor.DefaultOptions
   323  	// monitorOptions.Logger = log
   324  	monitorOptions.WithLogs = true
   325  	monitorOptions.BlockRetentionLimit = 1000
   326  
   327  	monitor, err := ethmonitor.NewMonitor(provider, monitorOptions)
   328  	assert.NoError(t, err)
   329  
   330  	go func() {
   331  		err := monitor.Run(ctx)
   332  		if err != nil {
   333  			t.Error(err)
   334  		}
   335  	}()
   336  
   337  	listenerOptions := ethreceipts.DefaultOptions
   338  	listenerOptions.NumBlocksToFinality = 10
   339  	listenerOptions.FilterMaxWaitNumBlocks = 4
   340  
   341  	receiptsListener, err := ethreceipts.NewReceiptsListener(log, provider, monitor, listenerOptions)
   342  	assert.NoError(t, err)
   343  
   344  	go func() {
   345  		err := receiptsListener.Run(ctx)
   346  		if err != nil {
   347  			t.Error(err)
   348  		}
   349  	}()
   350  
   351  	//
   352  	// Setup wallets
   353  	//
   354  
   355  	// create and fund a few wallets to send from
   356  	fromWallets, _ := testchain.DummyWallets(3, 100)
   357  	testchain.FundAddresses(ethtest.WalletAddresses(fromWallets), 10)
   358  
   359  	// create a few wallets to send to
   360  	toWallets, _ := testchain.DummyWallets(3, 200)
   361  
   362  	// prepare and sign bunch of txns
   363  	values := []*big.Int{}
   364  	for range fromWallets {
   365  		values = append(values, ethtest.ETHValue(0.1))
   366  	}
   367  
   368  	_, txns, err := ethtest.PrepareBlastSendTransactions(ctx, fromWallets, ethtest.WalletAddresses(toWallets), values)
   369  	assert.NoError(t, err)
   370  
   371  	// send the txns -- these will be async, so we can just blast synchronously
   372  	// and not have to do it in a goroutine
   373  	for _, txn := range txns {
   374  		_, _, err := ethtxn.SendTransaction(ctx, provider, txn)
   375  		assert.NoError(t, err)
   376  	}
   377  
   378  	//
   379  	// Subscribe to a filter on the receipt listener
   380  	//
   381  	fmt.Println("listening for txns..")
   382  
   383  	sub := receiptsListener.Subscribe(
   384  		ethreceipts.FilterFrom(fromWallets[1].Address()).LimitOne(true),
   385  		ethreceipts.FilterTo(toWallets[1].Address()),
   386  		ethreceipts.FilterTxnHash(txns[2].Hash()).ID(2222), //.Finalize(true) is set by default for FilterTxnHash
   387  	)
   388  
   389  	sub2 := receiptsListener.Subscribe()
   390  	sub2.AddFilter(ethreceipts.FilterTxnHash(txns[3].Hash()))
   391  
   392  	sub3 := receiptsListener.Subscribe(
   393  		ethreceipts.FilterTxnHash(txns[2].Hash()).ID(3333),
   394  
   395  		// will end up not being found and timeout after MaxWait
   396  		ethreceipts.FilterFrom(ethkit.Address{4, 2, 4, 2}).MaxWait(4),
   397  	)
   398  
   399  	go func() {
   400  		time.Sleep(5 * time.Second)
   401  		fmt.Println("==> delaying to find", txns[4].Hash().String())
   402  		sub.AddFilter(ethreceipts.FilterTxnHash(txns[4].Hash()).ID(4444))
   403  	}()
   404  
   405  	go func() {
   406  		for r := range sub2.TransactionReceipt() {
   407  			fmt.Println("sub2, got receipt", r.TransactionHash(), "final?", r.Final)
   408  		}
   409  	}()
   410  
   411  	go func() {
   412  		for r := range sub3.TransactionReceipt() {
   413  			fmt.Println("sub3, got receipt", r.TransactionHash(), "final?", r.Final, "id?", r.FilterID()) //, "maxWait hit?", r.Filter.IsExpired())
   414  		}
   415  	}()
   416  
   417  loop:
   418  	for {
   419  		select {
   420  
   421  		case <-ctx.Done():
   422  			fmt.Println("ctx done")
   423  			break loop
   424  
   425  		case <-sub.Done():
   426  			fmt.Println("sub done")
   427  			break loop
   428  
   429  		case receipt, ok := <-sub.TransactionReceipt():
   430  			if !ok {
   431  				continue
   432  			}
   433  
   434  			fmt.Println("=> sub, got receipt", receipt.TransactionHash(), "final?", receipt.Final, "id?", receipt.FilterID(), "status?", receipt.Status())
   435  
   436  			// txn := receipt.Transaction
   437  			// txnMsg := receipt.Message
   438  
   439  			fmt.Println("=> filter matched!", receipt.From(), receipt.TransactionHash())
   440  			fmt.Println("=> receipt status?", receipt.Status())
   441  
   442  			fmt.Println("==> len filters", len(sub.Filters()))
   443  			if receipt.TransactionHash() == txns[2].Hash() {
   444  				sub.RemoveFilter(receipt.Filter)
   445  			}
   446  			fmt.Println("==> len filters", len(sub.Filters()))
   447  
   448  			fmt.Println("")
   449  
   450  		// expecting to be finished with listening for events after a few seconds
   451  		case <-time.After(15 * time.Second):
   452  			sub.Unsubscribe()
   453  
   454  		}
   455  	}
   456  }
   457  
   458  func TestReceiptsListenerERC20(t *testing.T) {
   459  	ctx, cancel := context.WithCancel(context.Background())
   460  	defer cancel()
   461  
   462  	//
   463  	// Setup wallets and deploy erc20mock contract
   464  	//
   465  	wallet, _ := testchain.DummyWallet(1)
   466  	wallet2, _ := testchain.DummyWallet(2)
   467  	testchain.FundWallets(10, wallet, wallet2)
   468  
   469  	erc20Mock, _ := ethtest.DeployERC20Mock(t, testchain)
   470  
   471  	//
   472  	// Setup ReceiptsListener
   473  	//
   474  	provider := testchain.Provider
   475  
   476  	monitorOptions := ethmonitor.DefaultOptions
   477  	// monitorOptions.Logger = log
   478  	monitorOptions.WithLogs = true
   479  	monitorOptions.BlockRetentionLimit = 1000
   480  	monitorOptions.PollingInterval = 1000 * time.Millisecond
   481  
   482  	monitor, err := ethmonitor.NewMonitor(provider, monitorOptions)
   483  	assert.NoError(t, err)
   484  
   485  	go func() {
   486  		err := monitor.Run(ctx)
   487  		if err != nil {
   488  			t.Error(err)
   489  		}
   490  	}()
   491  
   492  	listenerOptions := ethreceipts.DefaultOptions
   493  	listenerOptions.NumBlocksToFinality = 10
   494  	listenerOptions.FilterMaxWaitNumBlocks = 4
   495  
   496  	receiptsListener, err := ethreceipts.NewReceiptsListener(log, provider, monitor, listenerOptions)
   497  	assert.NoError(t, err)
   498  
   499  	go func() {
   500  		err := receiptsListener.Run(ctx)
   501  		if err != nil {
   502  			t.Error(err)
   503  		}
   504  	}()
   505  
   506  	//
   507  	// Subscribe to a filter on the receipt listener
   508  	//
   509  	fmt.Println("listening for txns..")
   510  
   511  	erc20TransferTopic, err := erc20Mock.Contract.EventTopicHash("Transfer")
   512  	require.NoError(t, err)
   513  	_ = erc20TransferTopic
   514  
   515  	sub := receiptsListener.Subscribe(
   516  		ethreceipts.FilterLogTopic(erc20TransferTopic).Finalize(true).ID(9999).MaxWait(3),
   517  
   518  		// won't be found..
   519  		ethreceipts.FilterFrom(ethkit.Address{}).MaxWait(0).ID(8888),
   520  
   521  		// ethreceipts.FilterLogs(func(logs []*types.Log) bool {
   522  		// 	for _, log := range logs {
   523  		// 		if log.Address == erc20Mock.Contract.Address {
   524  		// 			return true
   525  		// 		}
   526  		// 		if log.Topics[0] == erc20TransferTopic {
   527  		// 			return true
   528  		// 		}
   529  
   530  		// 		// event := ethabi.DecodeERC20Log(log)
   531  		// 		// if event.From == "XXX"
   532  		// 	}
   533  		// 	return false
   534  		// }),
   535  	)
   536  
   537  	//
   538  	// Send some erc20 tokens
   539  	//
   540  	num := int64(2000)
   541  
   542  	erc20Receipts := make([]*types.Receipt, 0)
   543  	var erc20ReceiptsMu sync.Mutex
   544  
   545  	receipt := erc20Mock.Mint(t, wallet, num)
   546  	erc20Receipts = append(erc20Receipts, receipt)
   547  	erc20Mock.GetBalance(t, wallet.Address(), num)
   548  
   549  	go func() {
   550  		total := int64(0)
   551  		for i := 0; i < 5; i++ {
   552  			n := int64(40 + i)
   553  			total += n
   554  
   555  			erc20ReceiptsMu.Lock()
   556  			receipt := erc20Mock.Transfer(t, wallet, wallet2.Address(), n)
   557  			erc20Receipts = append(erc20Receipts, receipt)
   558  			erc20ReceiptsMu.Unlock()
   559  
   560  			erc20Mock.GetBalance(t, wallet2.Address(), total)
   561  		}
   562  	}()
   563  
   564  	//
   565  	// Listener loop
   566  	//
   567  	matchedCount := 0
   568  	matchedReceipts := make([]ethreceipts.Receipt, 0)
   569  
   570  loop:
   571  	for {
   572  		select {
   573  
   574  		case <-ctx.Done():
   575  			fmt.Println("ctx done")
   576  			break loop
   577  
   578  		case <-sub.Done():
   579  			fmt.Println("sub done")
   580  			break loop
   581  
   582  		case receipt, ok := <-sub.TransactionReceipt():
   583  			if !ok {
   584  				continue
   585  			}
   586  
   587  			matchedCount += 1
   588  			matchedReceipts = append(matchedReceipts, receipt)
   589  
   590  			fmt.Println("=> sub, got receipt", receipt.TransactionHash(), "final?", receipt.Final, "id?", receipt.FilterID(), "status?", receipt.Status())
   591  
   592  			// txn := receipt.Transaction
   593  			// txnMsg := receipt.Message
   594  
   595  			fmt.Println("=> filter matched!", receipt.From(), receipt.TransactionHash())
   596  			fmt.Println("=> receipt status?", receipt.Status())
   597  
   598  			fmt.Println("")
   599  
   600  		// expecting to be finished with listening for events after a few seconds
   601  		case <-time.After(25 * time.Second):
   602  			// NOTE: this should return 1 as there is a filter above with nolimit
   603  			fmt.Println("number of filters still remaining:", len(sub.Filters()))
   604  			sub.Unsubscribe()
   605  		}
   606  	}
   607  
   608  	// NOTE: expecting receipts twice. Once on mine, once on finalize.
   609  	for _, mr := range matchedReceipts {
   610  		found := false
   611  		for _, r := range erc20Receipts {
   612  			if mr.TransactionHash() == r.TxHash {
   613  				found = true
   614  			}
   615  		}
   616  		assert.True(t, found, "looking for matched receipt %s", mr.TransactionHash().String())
   617  	}
   618  
   619  	require.Equal(t, matchedCount, len(erc20Receipts)*2)
   620  }