@@ -4,13 +4,15 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "github.com/celestiaorg/tastora/framework/testutil/config"
78 "os"
89 "os/exec"
910 "path/filepath"
1011 "testing"
1112 "time"
1213
1314 sdkmath "cosmossdk.io/math"
15+ "github.com/BurntSushi/toml"
1416 "github.com/celestiaorg/tastora/framework/docker/container"
1517 "github.com/celestiaorg/tastora/framework/docker/cosmos"
1618 "github.com/celestiaorg/tastora/framework/docker/ibc"
@@ -19,12 +21,15 @@ import (
1921 "github.com/celestiaorg/tastora/framework/testutil/sdkacc"
2022 "github.com/celestiaorg/tastora/framework/testutil/wait"
2123 "github.com/celestiaorg/tastora/framework/types"
24+ cometcfg "github.com/cometbft/cometbft/config"
2225 "github.com/cosmos/cosmos-sdk/crypto/keyring"
2326 sdk "github.com/cosmos/cosmos-sdk/types"
2427 "github.com/cosmos/cosmos-sdk/types/module/testutil"
2528 "github.com/cosmos/cosmos-sdk/x/auth"
2629 "github.com/cosmos/cosmos-sdk/x/bank"
2730 banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
31+ "github.com/cosmos/ibc-go/v8/modules/apps/transfer"
32+ ibctransfer "github.com/cosmos/ibc-go/v8/modules/apps/transfer"
2833 transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
2934 clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
3035 "github.com/stretchr/testify/require"
@@ -49,8 +54,24 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() {
4954 }
5055 }()
5156
52- // Wait 10 seconds for GM chain to initialize
53- time .Sleep (8 * time .Second )
57+ // Wait for GM chain RPC to be ready
58+ s .T ().Log ("Waiting for GM chain RPC to be ready..." )
59+ var rpcReady bool
60+ for i := 0 ; i < 30 ; i ++ { // 30 second timeout
61+ node := gmChain .GetNodes ()[0 ]
62+ rpcClient , _ := node .GetRPCClient ()
63+ if rpcClient != nil {
64+ // Test if RPC client is actually working by making a simple call
65+ _ , err := rpcClient .Status (ctx )
66+ if err == nil {
67+ rpcReady = true
68+ s .T ().Log ("GM chain RPC is ready" )
69+ break
70+ }
71+ }
72+ time .Sleep (1 * time .Second )
73+ }
74+ require .True (s .T (), rpcReady , "GM chain RPC failed to become ready within 30 seconds" )
5475
5576 kr , err := gmChain .GetNodes ()[0 ].GetKeyring ()
5677 require .NoError (s .T (), err )
@@ -104,6 +125,10 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() {
104125 err = hermes .Init (ctx , s .celestiaChain , gmChain )
105126 require .NoError (s .T (), err , "failed to initialize relayer" )
106127
128+ // Switch hermes to pull mode to avoid WebSocket connection issues
129+ err = s .configureHermesForPullMode (ctx , hermes )
130+ require .NoError (s .T (), err , "failed to configure hermes for pull mode" )
131+
107132 connection , channel := setupIBCConnection (s .T (), ctx , s .celestiaChain , gmChain , hermes )
108133 s .T ().Logf ("Established IBC connection %s and channel %s between Celestia and GM chain" , connection .ConnectionID , channel .ChannelID )
109134
@@ -168,7 +193,8 @@ func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *c
168193 s .T ().Log ("Setting up attester keyring with validator key..." )
169194
170195 // Create an in-memory keyring for the attester
171- testEncCfg := testutil .MakeTestEncodingConfig (auth.AppModuleBasic {}, bank.AppModuleBasic {})
196+ // Include transfer module so MsgTransfer is registered in the interface registry
197+ testEncCfg := testutil .MakeTestEncodingConfig (auth.AppModuleBasic {}, bank.AppModuleBasic {}, ibctransfer.AppModuleBasic {})
172198 attesterKeyring := keyring .NewInMemory (testEncCfg .Codec )
173199
174200 // Import the validator key into the attester keyring
@@ -216,6 +242,54 @@ func deriveAttesterAccountFromArmor(armoredKey string) (sdk.AccAddress, error) {
216242 return keyAddr , nil
217243}
218244
245+ func (s * DockerIntegrationTestSuite ) configureHermesForPullMode (ctx context.Context , hermes * relayer.Hermes ) error {
246+ s .T ().Log ("Configuring hermes to use pull mode and increased clock drift..." )
247+
248+ // Read the current config
249+ configBz , err := hermes .ReadFile (ctx , ".hermes/config.toml" )
250+ if err != nil {
251+ return fmt .Errorf ("failed to read hermes config: %w" , err )
252+ }
253+
254+ // Unmarshal into map
255+ var config map [string ]interface {}
256+ if err := toml .Unmarshal (configBz , & config ); err != nil {
257+ return fmt .Errorf ("failed to unmarshal hermes config: %w" , err )
258+ }
259+
260+ // Update chains to use pull mode and increase clock drift
261+ chains , ok := config ["chains" ].([]map [string ]interface {})
262+ if ! ok {
263+ return fmt .Errorf ("chains not found in config or not in expected format" )
264+ }
265+
266+ for i := range chains {
267+ // Update event_source to pull mode with tighter interval
268+ chains [i ]["event_source" ] = map [string ]interface {}{
269+ "mode" : "pull" ,
270+ "interval" : "200ms" ,
271+ }
272+
273+ // Update clock_drift to handle timing differences between chains
274+ chains [i ]["clock_drift" ] = "60s"
275+ }
276+
277+ // Remarshal into bytes
278+ updatedConfigBz , err := toml .Marshal (config )
279+ if err != nil {
280+ return fmt .Errorf ("failed to marshal updated hermes config: %w" , err )
281+ }
282+
283+ // Write the updated config
284+ err = hermes .WriteFile (ctx , ".hermes/config.toml" , updatedConfigBz )
285+ if err != nil {
286+ return fmt .Errorf ("failed to write updated hermes config: %w" , err )
287+ }
288+
289+ s .T ().Log ("Successfully configured hermes to use pull mode with 60s clock drift tolerance" )
290+ return nil
291+ }
292+
219293func (s * DockerIntegrationTestSuite ) buildGMImage () {
220294 evnodeVersion := getenvDefault ("EVNODE_VERSION" , "v1.0.0-beta.4" )
221295 igniteVersion := getenvDefault ("IGNITE_VERSION" , "v29.3.1" )
@@ -247,7 +321,7 @@ func (s *DockerIntegrationTestSuite) getGmChain(ctx context.Context) *cosmos.Cha
247321 s .T ().Log ("Creating GM chain connected to DA network..." )
248322 sdk .GetConfig ().SetBech32PrefixForAccount ("celestia" , "celestiapub" )
249323 gmImg := container .NewImage ("evabci/gm" , "local" , "1000:1000" )
250- testEncCfg := testutil .MakeTestEncodingConfig (auth.AppModuleBasic {}, bank.AppModuleBasic {})
324+ testEncCfg := testutil .MakeTestEncodingConfig (auth.AppModuleBasic {}, bank.AppModuleBasic {}, transfer. AppModuleBasic {} )
251325 gmChain , err := cosmos .NewChainBuilder (s .T ()).
252326 WithEncodingConfig (& testEncCfg ).
253327 WithDockerClient (s .dockerClient ).
@@ -276,6 +350,42 @@ func (s *DockerIntegrationTestSuite) getGmChain(ctx context.Context) *cosmos.Cha
276350 "--minimum-gas-prices" , "0.001stake" ,
277351 "--log_level" , "*:info" ,
278352 ).
353+ WithPostInit (func (ctx context.Context , node * cosmos.ChainNode ) error {
354+ // 1) Ensure ABCI responses and tx events are retained and indexed for Hermes
355+ if err := config .Modify (ctx , node , "config/config.toml" , func (cfg * cometcfg.Config ) {
356+ cfg .Storage .DiscardABCIResponses = false
357+ // Enable key-value tx indexer so Hermes can query IBC packet events
358+ cfg .TxIndex .Indexer = "kv"
359+ // Increase RPC BroadcastTxCommit timeout to accommodate CI slowness
360+ if cfg .RPC != nil {
361+ cfg .RPC .TimeoutBroadcastTxCommit = 120000000000 // 120s in nanoseconds
362+ }
363+ }); err != nil {
364+ return err
365+ }
366+ // 2) Ensure app-level index-events include IBC packet events
367+ appToml , err := node .ReadFile (ctx , "config/app.toml" )
368+ if err != nil {
369+ return err
370+ }
371+ var appCfg map [string ]interface {}
372+ if err := toml .Unmarshal (appToml , & appCfg ); err != nil {
373+ return err
374+ }
375+ appCfg ["index-events" ] = []string {
376+ "message.action" ,
377+ "send_packet" ,
378+ "recv_packet" ,
379+ "write_acknowledgement" ,
380+ "acknowledge_packet" ,
381+ "timeout_packet" ,
382+ }
383+ updated , err := toml .Marshal (appCfg )
384+ if err != nil {
385+ return err
386+ }
387+ return node .WriteFile (ctx , "config/app.toml" , updated )
388+ }).
279389 WithNode (cosmos .NewChainNodeConfigBuilder ().
280390 WithPostInit (AddSingleSequencer ).
281391 Build ()).
@@ -365,6 +475,9 @@ func setupIBCConnection(t *testing.T, ctx context.Context, chainA, chainB types.
365475 require .NoError (t , err )
366476 require .NotEmpty (t , connection .ConnectionID , "Connection ID should not be empty" )
367477
478+ // give chains a moment to persist connection state and client updates
479+ _ = wait .ForBlocks (ctx , 2 , chainA , chainB )
480+
368481 // Create an ICS20 channel for token transfers
369482 channelOpts := ibc.CreateChannelOptions {
370483 SourcePortName : "transfer" ,
@@ -409,6 +522,9 @@ func (s *DockerIntegrationTestSuite) testIBCTransfers(ctx context.Context, celes
409522 err = hermes .Start (ctx )
410523 require .NoError (s .T (), err )
411524
525+ // Allow Hermes to sync initial heights before sending packets
526+ _ = wait .ForBlocks (ctx , 2 , celestiaChain , gmChain )
527+
412528 // Test 1: Transfer from Celestia to GM chain
413529 s .T ().Log ("=== Testing transfer from Celestia to GM chain ===" )
414530
@@ -428,16 +544,18 @@ func (s *DockerIntegrationTestSuite) testIBCTransfers(ctx context.Context, celes
428544 "" ,
429545 )
430546
431- resp , err := celestiaChain .BroadcastMessages (ctx , celestiaWallet , transferMsg )
547+ // Use a longer per-tx timeout to avoid 60s default aborts on busy or lagging nodes
548+ ctxTx , cancelTx := context .WithTimeout (ctx , 2 * time .Minute )
549+ defer cancelTx ()
550+ resp , err := celestiaChain .BroadcastMessages (ctxTx , celestiaWallet , transferMsg )
432551 require .NoError (s .T (), err )
433552 require .Equal (s .T (), uint32 (0 ), resp .Code , "IBC transfer failed: %s" , resp .RawLog )
434553
435554 s .T ().Logf ("IBC transfer broadcast successful. TX hash: %s" , resp .TxHash )
436555
437- // Wait for transfer to be relayed
438- s .T ().Log ("Waiting for transfer to be relayed..." )
439- err = wait .ForBlocks (ctx , 10 , celestiaChain , gmChain )
440- require .NoError (s .T (), err )
556+ // Wait until GM balance reflects the transfer (poll with timeout)
557+ s .T ().Log ("Waiting for GM balance to update..." )
558+ require .NoError (s .T (), s .waitForBalanceIncrease (ctx , gmChain , gmAddr , celestiaToGMIBCDenom , initialGMBalance , transferAmount , 2 * time .Minute ))
441559
442560 // Check final balance
443561 finalGMBalance := s .getBalance (ctx , gmChain , gmAddr , celestiaToGMIBCDenom )
@@ -470,16 +588,18 @@ func (s *DockerIntegrationTestSuite) testIBCTransfers(ctx context.Context, celes
470588 "" ,
471589 )
472590
473- resp , err = gmChain .BroadcastMessages (ctx , gmWallet , reverseTransferMsg )
591+ // Use the same extended timeout for the reverse transfer
592+ ctxTx2 , cancelTx2 := context .WithTimeout (ctx , 2 * time .Minute )
593+ defer cancelTx2 ()
594+ resp , err = gmChain .BroadcastMessages (ctxTx2 , gmWallet , reverseTransferMsg )
474595 require .NoError (s .T (), err )
475596 require .Equal (s .T (), uint32 (0 ), resp .Code , "Reverse IBC transfer failed: %s" , resp .RawLog )
476597
477598 s .T ().Logf ("Reverse IBC transfer broadcast successful. TX hash: %s" , resp .TxHash )
478599
479- // Wait for reverse transfer to be relayed
480- s .T ().Log ("Waiting for reverse transfer to be relayed..." )
481- err = wait .ForBlocks (ctx , 10 , celestiaChain , gmChain )
482- require .NoError (s .T (), err )
600+ // Wait until Celestia balance reflects the transfer (poll with timeout)
601+ s .T ().Log ("Waiting for Celestia balance to update..." )
602+ require .NoError (s .T (), s .waitForBalanceIncrease (ctx , celestiaChain , celestiaAddr , gmToCelestiaIBCDenom , initialCelestiaBalance , transferAmount , 2 * time .Minute ))
483603
484604 // Check final balance
485605 finalCelestiaBalance := s .getBalance (ctx , celestiaChain , celestiaAddr , gmToCelestiaIBCDenom )
@@ -493,6 +613,26 @@ func (s *DockerIntegrationTestSuite) testIBCTransfers(ctx context.Context, celes
493613 s .T ().Log ("=== IBC Transfer Tests Completed Successfully ===" )
494614}
495615
616+ // waitForBalanceIncrease polls the balance until it increases by expectedIncrease or timeout expires.
617+ func (s * DockerIntegrationTestSuite ) waitForBalanceIncrease (ctx context.Context , chain * cosmos.Chain , address sdk.AccAddress , denom string , initial sdkmath.Int , expectedIncrease sdkmath.Int , timeout time.Duration ) error {
618+ deadline := time .Now ().Add (timeout )
619+ target := initial .Add (expectedIncrease )
620+ for {
621+ current := s .getBalance (ctx , chain , address , denom )
622+ if current .GTE (target ) {
623+ return nil
624+ }
625+ if time .Now ().After (deadline ) {
626+ return fmt .Errorf ("balance did not reach target within %s: got %s, want %s (%s)" , timeout , current .String (), target .String (), denom )
627+ }
628+ select {
629+ case <- ctx .Done ():
630+ return ctx .Err ()
631+ case <- time .After (1 * time .Second ):
632+ }
633+ }
634+ }
635+
496636// getBalance queries the balance of an address for a specific denom
497637func (s * DockerIntegrationTestSuite ) getBalance (ctx context.Context , chain * cosmos.Chain , address sdk.AccAddress , denom string ) sdkmath.Int {
498638 node := chain .GetNode ()
0 commit comments