decred.org/dcrdex@v1.0.5/client/asset/eth/multirpc_test_util.go (about)

     1  //go:build rpclive
     2  
     3  package eth
     4  
     5  import (
     6  	"context"
     7  	"flag"
     8  	"fmt"
     9  	"math/big"
    10  	"math/rand"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"decred.org/dcrdex/client/asset"
    19  	"decred.org/dcrdex/dex"
    20  	"decred.org/dcrdex/dex/encode"
    21  	dexeth "decred.org/dcrdex/dex/networks/eth"
    22  	"github.com/ethereum/go-ethereum/common"
    23  	"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
    24  	"github.com/ethereum/go-ethereum/params"
    25  )
    26  
    27  type MRPCTest struct {
    28  	ctx               context.Context
    29  	chain             string
    30  	chainConfigLookup func(net dex.Network) (*params.ChainConfig, error)
    31  	compatDataLookup  func(net dex.Network) (CompatibilityData, error)
    32  	harnessDirectory  string
    33  	credentialsFile   string
    34  }
    35  
    36  // NewMRPCTest creates a new MRPCTest.
    37  // Create a credntials.json file in your ~/dextest directory.
    38  // See README for getgas for format
    39  func NewMRPCTest(
    40  	ctx context.Context,
    41  	cfgLookup func(net dex.Network) (*params.ChainConfig, error),
    42  	compatLookup func(net dex.Network) (c CompatibilityData, err error),
    43  	chainSymbol string,
    44  ) *MRPCTest {
    45  
    46  	var skipWS bool
    47  	flag.BoolVar(&skipWS, "skipws", false, "skip attempt to automatically resolve WebSocket URL from HTTP(S) URL")
    48  	flag.Parse()
    49  	if skipWS {
    50  		forceTryWS = false
    51  	}
    52  
    53  	dextestDir := filepath.Join(os.Getenv("HOME"), "dextest")
    54  	return &MRPCTest{
    55  		ctx:               ctx,
    56  		chain:             chainSymbol,
    57  		chainConfigLookup: cfgLookup,
    58  		compatDataLookup:  compatLookup,
    59  		harnessDirectory:  filepath.Join(dextestDir, chainSymbol, "harness-ctl"),
    60  		credentialsFile:   filepath.Join(dextestDir, "credentials.json"),
    61  	}
    62  }
    63  
    64  func (m *MRPCTest) rpcClient(dir string, seed []byte, endpoints []string, net dex.Network, skipConnect bool) (*multiRPCClient, error) {
    65  	wDir := getWalletDir(dir, net)
    66  	err := os.MkdirAll(wDir, 0755)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("os.Mkdir error: %w", err)
    69  	}
    70  
    71  	log := dex.StdOutLogger("T", dex.LevelTrace)
    72  
    73  	cfg, err := m.chainConfigLookup(net)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("chainConfigLookup error: %v", err)
    76  	}
    77  
    78  	compat, err := m.compatDataLookup(net)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("compatDataLookup error: %v", err)
    81  	}
    82  
    83  	chainID := cfg.ChainID.Int64()
    84  	if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{
    85  		Type: walletTypeRPC,
    86  		Seed: seed,
    87  		Pass: []byte("abc"),
    88  		Settings: map[string]string{
    89  			"providers": strings.Join(endpoints, " "),
    90  		},
    91  		DataDir: dir,
    92  		Net:     net,
    93  		Logger:  dex.StdOutLogger("T", dex.LevelTrace),
    94  	}, &compat, skipConnect); err != nil {
    95  		return nil, fmt.Errorf("error creating wallet: %v", err)
    96  	}
    97  
    98  	return newMultiRPCClient(dir, endpoints, log, cfg, 3, net)
    99  }
   100  
   101  func (m *MRPCTest) TestHTTP(t *testing.T, port string) {
   102  	if err := m.testSimnetEndpoint([]string{"http://localhost:" + port}, 2, nil); err != nil {
   103  		t.Fatal(err)
   104  	}
   105  }
   106  
   107  func (m *MRPCTest) TestWS(t *testing.T, port string) {
   108  	if err := m.testSimnetEndpoint([]string{"ws://localhost:" + port}, 2, nil); err != nil {
   109  		t.Fatal(err)
   110  	}
   111  }
   112  
   113  func (m *MRPCTest) TestWSTxLogs(t *testing.T, port string) {
   114  	if err := m.testSimnetEndpoint([]string{"ws://localhost:" + port}, 2, func(ctx context.Context, cl *multiRPCClient) {
   115  		for i := 0; i < 3; i++ {
   116  			time.Sleep(time.Second)
   117  			m.harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "1")
   118  			m.mine(ctx)
   119  
   120  		}
   121  		bal, _ := cl.addressBalance(ctx, cl.creds.addr)
   122  		fmt.Println("Balance after", bal)
   123  	}); err != nil {
   124  		t.Fatal(err)
   125  	}
   126  }
   127  
   128  func (m *MRPCTest) TestSimnetMultiRPCClient(t *testing.T, wsPort, httpPort string) {
   129  	endpoints := []string{
   130  		"ws://localhost:" + wsPort,
   131  		"http://localhost:" + httpPort,
   132  	}
   133  
   134  	nonceProviderStickiness = time.Second / 2
   135  
   136  	rand.Seed(time.Now().UnixNano())
   137  
   138  	if err := m.testSimnetEndpoint(endpoints, 2, func(ctx context.Context, cl *multiRPCClient) {
   139  		// Get some funds
   140  		m.harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "3")
   141  
   142  		time.Sleep(time.Second)
   143  		m.mine(ctx)
   144  		time.Sleep(time.Second)
   145  
   146  		if err := cl.unlock("abc"); err != nil {
   147  			t.Fatalf("error unlocking: %v", err)
   148  		}
   149  
   150  		const amt = 1e8 // 0.1 ETH
   151  		var alphaAddr = common.HexToAddress("0x18d65fb8d60c1199bb1ad381be47aa692b482605")
   152  
   153  		for i := 0; i < 10; i++ {
   154  			// Send two in a row. They should use each provider, preferred first.
   155  			for j := 0; j < 2; j++ {
   156  				txOpts, err := cl.txOpts(ctx, amt, defaultSendGasLimit, nil, nil, nil)
   157  				if err != nil {
   158  					t.Fatal(err)
   159  				}
   160  				if _, err := cl.sendTransaction(ctx, txOpts, alphaAddr, nil); err != nil {
   161  					t.Fatalf("error sending tx %d-%d: %v", i, j, err)
   162  				}
   163  			}
   164  			_, err := cl.bestHeader(ctx)
   165  			if err != nil {
   166  				t.Fatalf("bestHeader error: %v", err)
   167  			}
   168  			// Let nonce provider expire. The pair should use the same
   169  			// provider, but different pairs can use different providers. Look
   170  			// for the variation.
   171  			m.mine(ctx)
   172  			time.Sleep(time.Second)
   173  		}
   174  	}); err != nil {
   175  		t.Fatal(err)
   176  	}
   177  }
   178  
   179  func (m *MRPCTest) TestMonitorNet(t *testing.T, net dex.Network) {
   180  	seed, providers := m.readProviderFile(t, net)
   181  	dir, _ := os.MkdirTemp("", "")
   182  	defer os.RemoveAll(dir)
   183  
   184  	cl, err := m.rpcClient(dir, seed, providers, net, true)
   185  	if err != nil {
   186  		t.Fatal(err)
   187  	}
   188  
   189  	ctx, cancel := context.WithTimeout(m.ctx, time.Hour)
   190  	defer cancel()
   191  
   192  	if err := cl.connect(ctx); err != nil {
   193  		t.Fatalf("Connection error: %v", err)
   194  	}
   195  	<-ctx.Done()
   196  }
   197  
   198  func (m *MRPCTest) TestRPC(t *testing.T, net dex.Network) {
   199  	// To skip automatic websocket resolution, pass flag --skipws.
   200  
   201  	endpoint := os.Getenv("PROVIDER")
   202  	if endpoint == "" {
   203  		t.Fatalf("specify a provider in the PROVIDER environmental variable")
   204  	}
   205  	dir, _ := os.MkdirTemp("", "")
   206  	defer os.RemoveAll(dir)
   207  	cl, err := m.rpcClient(dir, encode.RandomBytes(32), []string{endpoint}, net, true)
   208  	if err != nil {
   209  		t.Fatal(err)
   210  	}
   211  
   212  	if err := cl.connect(m.ctx); err != nil {
   213  		t.Fatalf("connect error: %v", err)
   214  	}
   215  
   216  	compat, err := m.compatDataLookup(net)
   217  	if err != nil {
   218  		t.Fatalf("compatDataLookup error: %v", err)
   219  	}
   220  
   221  	for _, tt := range newCompatibilityTests(cl, &compat, cl.log) {
   222  		tStart := time.Now()
   223  		if err := cl.withAny(m.ctx, tt.f); err != nil {
   224  			t.Fatalf("%q: %v", tt.name, err)
   225  		}
   226  		fmt.Printf("### %q: %s \n", tt.name, time.Since(tStart))
   227  	}
   228  }
   229  
   230  func (m *MRPCTest) TestFreeServers(t *testing.T, freeServers []string, net dex.Network) {
   231  	compat, err := m.compatDataLookup(net)
   232  	if err != nil {
   233  		t.Fatalf("compatDataLookup error: %v", err)
   234  	}
   235  	runTest := func(endpoint string) error {
   236  		dir, _ := os.MkdirTemp("", "")
   237  		defer os.RemoveAll(dir)
   238  		cl, err := m.rpcClient(dir, encode.RandomBytes(32), []string{endpoint}, net, true)
   239  		if err != nil {
   240  			return fmt.Errorf("tRPCClient error: %v", err)
   241  		}
   242  		if err := cl.connect(m.ctx); err != nil {
   243  			return fmt.Errorf("connect error: %v", err)
   244  		}
   245  		for _, tt := range newCompatibilityTests(cl, &compat, cl.log) {
   246  			if err := cl.withAny(m.ctx, tt.f); err != nil {
   247  				return fmt.Errorf("%q error: %v", tt.name, err)
   248  			}
   249  			fmt.Printf("#### %q passed %q \n", endpoint, tt.name)
   250  		}
   251  		return nil
   252  	}
   253  
   254  	passes, fails := make([]string, 0), make(map[string]error, 0)
   255  	for _, endpoint := range freeServers {
   256  		if err := runTest(endpoint); err != nil {
   257  			fails[endpoint] = err
   258  		} else {
   259  			passes = append(passes, endpoint)
   260  		}
   261  	}
   262  	for _, pass := range passes {
   263  		fmt.Printf("!!!! %q PASSED \n", pass)
   264  	}
   265  	for endpoint, err := range fails {
   266  		fmt.Printf("XXXX %q FAILED : %v \n", endpoint, err)
   267  	}
   268  }
   269  
   270  func (m *MRPCTest) TestMainnetCompliance(t *testing.T) {
   271  	_, providerLookup := m.readProviderFile(t, dex.Mainnet)
   272  	ctx, cancel := context.WithCancel(context.Background())
   273  	defer cancel()
   274  
   275  	cfg, err := m.chainConfigLookup(dex.Mainnet)
   276  	if err != nil {
   277  		t.Fatalf("chainConfigLookup error: %v", err)
   278  	}
   279  
   280  	compat, err := m.compatDataLookup(dex.Mainnet)
   281  	if err != nil {
   282  		t.Fatalf("compatDataLookup error: %v", err)
   283  	}
   284  
   285  	log := dex.StdOutLogger("T", dex.LevelTrace)
   286  	providers, err := connectProviders(ctx, providerLookup, log, cfg.ChainID, dex.Mainnet)
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  	_, err = checkProvidersCompliance(ctx, providers, &compat, log, true)
   291  	if err != nil {
   292  		t.Fatal(err)
   293  	}
   294  }
   295  
   296  func (m *MRPCTest) TestReceiptsHaveEffectiveGasPrice(t *testing.T) {
   297  	m.withClient(t, dex.Mainnet, func(ctx context.Context, cl *multiRPCClient) {
   298  		if err := cl.withAny(ctx, func(ctx context.Context, p *provider) error {
   299  			blk, err := p.ec.BlockByNumber(ctx, nil)
   300  			if err != nil {
   301  				return fmt.Errorf("BlockByNumber error: %v", err)
   302  			}
   303  			h := blk.Number()
   304  			const m = 20 // how many txs
   305  			var n int
   306  			for n < m {
   307  				txs := blk.Transactions()
   308  				fmt.Printf("##### Block %d has %d transactions", h, len(txs))
   309  				for _, tx := range txs {
   310  					n++
   311  					r, err := cl.transactionReceipt(ctx, tx.Hash())
   312  					if err != nil {
   313  						return fmt.Errorf("transactionReceipt error: %v", err)
   314  					}
   315  					if r.EffectiveGasPrice != nil {
   316  						fmt.Printf("##### Effective gas price: %s \n", r.EffectiveGasPrice)
   317  					} else {
   318  						fmt.Printf("##### No effective gas price for tx %s \n", tx.Hash())
   319  					}
   320  				}
   321  				h.Add(h, big.NewInt(-1))
   322  				blk, err = p.ec.BlockByNumber(ctx, h)
   323  				if err != nil {
   324  					return fmt.Errorf("error getting block %d: %w", h, err)
   325  				}
   326  			}
   327  			return nil
   328  		}); err != nil {
   329  			t.Fatal(err)
   330  		}
   331  	})
   332  }
   333  
   334  func (m *MRPCTest) withClient(t *testing.T, net dex.Network, f func(context.Context, *multiRPCClient)) {
   335  	seed, providers := m.readProviderFile(t, net)
   336  	dir, _ := os.MkdirTemp("", "")
   337  	defer os.RemoveAll(dir)
   338  
   339  	cl, err := m.rpcClient(dir, seed, providers, net, false)
   340  	if err != nil {
   341  		t.Fatalf("Error creating rpc client: %v", err)
   342  	}
   343  
   344  	ctx, cancel := context.WithTimeout(m.ctx, time.Hour)
   345  	defer cancel()
   346  
   347  	if err := cl.connect(ctx); err != nil {
   348  		t.Fatalf("Connection error: %v", err)
   349  	}
   350  
   351  	f(ctx, cl)
   352  }
   353  
   354  // FeeHistory prints the base fees sampled once per week going back the
   355  // specified number of days.
   356  func (m *MRPCTest) FeeHistory(t *testing.T, net dex.Network, blockTimeSecs, days uint64) {
   357  	m.withClient(t, net, func(ctx context.Context, cl *multiRPCClient) {
   358  		tip, err := cl.bestHeader(ctx)
   359  		if err != nil {
   360  			t.Fatalf("bestHeader error: %v", err)
   361  		}
   362  
   363  		tipHeight := tip.Number.Uint64()
   364  
   365  		baseFees := eip1559.CalcBaseFee(cl.cfg, tip)
   366  
   367  		fmt.Printf("##### Tip = %d \n", tipHeight)
   368  		fmt.Printf("##### Current base fees: %s \n", fmtFee(baseFees))
   369  
   370  		const secondsPerDay = 86_400
   371  		var samplingDuration uint64 = 7 * secondsPerDay // Check every 7 days
   372  		totalDuration := secondsPerDay * days
   373  		n := totalDuration / samplingDuration
   374  		samplingDistance := samplingDuration / blockTimeSecs
   375  		fees := make([]uint64, n)
   376  		for i := range fees {
   377  			height := tipHeight - (uint64(i+1) * samplingDistance)
   378  			hdr, err := cl.HeaderByNumber(ctx, big.NewInt(int64(height)))
   379  			if err != nil {
   380  				t.Fatalf("HeaderByNumber(%d) error: %v", height, err)
   381  			}
   382  			if hdr.BaseFee == nil {
   383  				fmt.Println("nil base fees for height", height)
   384  				continue
   385  			}
   386  			baseFees = eip1559.CalcBaseFee(cl.cfg, hdr)
   387  			fmt.Printf("##### Base fees height %d @ %s: %s \n", height, time.Unix(int64(hdr.Time), 0), fmtFee(baseFees))
   388  		}
   389  	})
   390  }
   391  
   392  func (m *MRPCTest) TipCaps(t *testing.T, net dex.Network) {
   393  	m.withClient(t, net, func(ctx context.Context, cl *multiRPCClient) {
   394  		if err := cl.withAny(ctx, func(ctx context.Context, p *provider) error {
   395  			blk, err := p.ec.BlockByNumber(ctx, nil)
   396  			if err != nil {
   397  				return err
   398  			}
   399  			h := blk.Number()
   400  			const m = 20 // how many txs
   401  			var n int
   402  			for {
   403  				txs := blk.Transactions()
   404  				fmt.Printf("##### Block %d has %d transactions \n", h, len(txs))
   405  				for _, tx := range txs {
   406  					n++
   407  					fmt.Println("##### Tx tip cap =", fmtFee(tx.GasTipCap()))
   408  				}
   409  				if n >= m {
   410  					break
   411  				}
   412  				h.Add(h, big.NewInt(-1))
   413  				blk, err = p.ec.BlockByNumber(ctx, h)
   414  				if err != nil {
   415  					return fmt.Errorf("error getting block %d: %w", h, err)
   416  				}
   417  			}
   418  
   419  			return nil
   420  		}); err != nil {
   421  			t.Fatalf("Error getting block: %v", err)
   422  		}
   423  	})
   424  }
   425  
   426  func (m *MRPCTest) testSimnetEndpoint(endpoints []string, syncBlocks uint64, tFunc func(context.Context, *multiRPCClient)) error {
   427  	dir, _ := os.MkdirTemp("", "")
   428  	defer os.RemoveAll(dir)
   429  
   430  	cl, err := m.rpcClient(dir, encode.RandomBytes(32), endpoints, dex.Simnet, false)
   431  	if err != nil {
   432  		return err
   433  	}
   434  	fmt.Println("######## Address:", cl.creds.addr)
   435  
   436  	if err := cl.connect(m.ctx); err != nil {
   437  		return fmt.Errorf("connect error: %v", err)
   438  	}
   439  
   440  	startHdr, err := cl.bestHeader(m.ctx)
   441  	if err != nil {
   442  		return fmt.Errorf("error getting initial header: %v", err)
   443  	}
   444  
   445  	// mine headers
   446  	start := time.Now()
   447  	for {
   448  		m.mine(m.ctx)
   449  		hdr, err := cl.bestHeader(m.ctx)
   450  		if err != nil {
   451  			return fmt.Errorf("error getting best header: %v", err)
   452  		}
   453  		blocksMined := hdr.Number.Uint64() - startHdr.Number.Uint64()
   454  		if blocksMined > syncBlocks {
   455  			break
   456  		}
   457  		if time.Since(start) > time.Minute {
   458  			return fmt.Errorf("timed out")
   459  		}
   460  		select {
   461  		case <-time.After(time.Second * 5):
   462  			// mine and check again
   463  		case <-m.ctx.Done():
   464  			return context.Canceled
   465  		}
   466  	}
   467  
   468  	if tFunc != nil {
   469  		tFunc(m.ctx, cl)
   470  	}
   471  
   472  	time.Sleep(time.Second)
   473  
   474  	return nil
   475  }
   476  
   477  func (m *MRPCTest) harnessCmd(ctx context.Context, exe string, args ...string) (string, error) {
   478  	cmd := exec.CommandContext(ctx, exe, args...)
   479  	cmd.Dir = m.harnessDirectory
   480  	op, err := cmd.CombinedOutput()
   481  	return string(op), err
   482  }
   483  
   484  func (m *MRPCTest) mine(ctx context.Context) error {
   485  	_, err := m.harnessCmd(ctx, "./mine-alpha", "1")
   486  	return err
   487  }
   488  
   489  func (m *MRPCTest) readProviderFile(t *testing.T, net dex.Network) (seed []byte, providers []string) {
   490  	t.Helper()
   491  	var err error
   492  	seed, providers, err = getFileCredentials(m.chain, m.credentialsFile, net)
   493  	if err != nil {
   494  		t.Fatalf("Error retreiving credentials from file at %q: %v", m.credentialsFile, err)
   495  	}
   496  	return
   497  }
   498  
   499  func fmtFee(v *big.Int) string {
   500  	if v.Cmp(dexeth.GweiToWei(1)) < 0 {
   501  		return fmt.Sprintf("%s wei / gas", v)
   502  	}
   503  	return fmt.Sprintf("%d gwei / gas", dexeth.WeiToGwei(v))
   504  }