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 }