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 := ðtxn.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 }