Skip to content

Conversation

@rabi-siddique
Copy link
Contributor

@rabi-siddique rabi-siddique commented Jan 7, 2026

closes:

Description

Removes the dependency on AxelarScan for GMP transaction failure status detection. Instead, it now uses Alchemy WebSocket connections to detect failed GMP transactions on EVM. Alchemy WebSockets are used for both failure and success detection.

This functionality is currently implemented in the live mode of the GMP watcher. Support for lookback mode will be added in a subsequent PR, as detecting reverted transactions in historical logs requires a different mechanism.

Testing Considerations

Existing tests have been adapted to the new changes. Mocks were updated as well. And couple of new tests were added for testing revert behavior.

Upgrade Considerations

  • Requires a new planner deployment.
  • Requires updates to the monitoring and alerting infrastructure to correctly filter and interpret GMP transaction logs.

@rabi-siddique rabi-siddique force-pushed the rs-find-reverted-transactions branch 2 times, most recently from f04b63d to 28b2ce9 Compare January 8, 2026 11:35
@rabi-siddique rabi-siddique changed the title chore: find reverted gmp transactions chore: find reverted gmp transactions in live mode Jan 8, 2026
@rabi-siddique
Copy link
Contributor Author

Logs from earlier e2e testing specifically if tx is made by the Wallet Owner:

errrior from call: Error: execution reverted (unknown custom error) (action="call", data="0x97232f6a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000047478353500000000000000000000000000000000000000000000000000000000", reason=null, transaction={ "data": "0x49160658fc151a77dfc86b108ef0922de0a59b7c613e94b8a1a5fcf8277e9c1f35b5db1e000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000661676f7269630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d61676f726963317277776c65793535306b396d6d6b367571366d6d367a3475647267386b7975797666737a6a6b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747835350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000927c000000000000000000000000000000000000000000000000000000000000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000084617ba03700000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000927c00000000000000000000000007a4150a9bf3bee704493b7178c35b7c174b4f6ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "from": "0xD36aac0c9676e984D72823Fb662ce94D3Ab5E551", "to": "0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.13.4)
    at makeError (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/utils/errors.ts:694:21)
    at getBuiltinCallException (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/abi/abi-coder.ts:118:12)
    at Function.getBuiltinCallException (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/abi/abi-coder.ts:235:16)
    at WebSocketProvider.getRpcError (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:989:32)
    at file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:563:45 {
  code: 'CALL_EXCEPTION',
  action: 'call',
  data: '0x97232f6a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000047478353500000000000000000000000000000000000000000000000000000000',
  reason: null,
  transaction: {
    to: '0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF',
    data: '0x49160658fc151a77dfc86b108ef0922de0a59b7c613e94b8a1a5fcf8277e9c1f35b5db1e000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000661676f7269630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d61676f726963317277776c65793535306b396d6d6b367571366d6d367a3475647267386b7975797666737a6a6b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747835350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000927c000000000000000000000000000000000000000000000000000000000000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000084617ba03700000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000927c00000000000000000000000007a4150a9bf3bee704493b7178c35b7c174b4f6ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    from: '0xD36aac0c9676e984D72823Fb662ce94D3Ab5E551'
  },
  invocation: null,
  revert: null,
  shortMessage: 'execution reverted (unknown custom error)',
  info: {
    error: {
      code: 3,
      message: 'execution reverted',
      data: '0x97232f6a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000047478353500000000000000000000000000000000000000000000000000000000'
    },
    payload: { method: 'eth_call', params: [Array], id: 6, jsonrpc: '2.0' }
  }
}
revertData: 0x97232f6a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000047478353500000000000000000000000000000000000000000000000000000000
❌ REVERTED: txId=tx55 txHash=0x08cf25716266f35b89b9b64f090b66ac58170414a50dfcea4f5581f15bc14541 block=231640080 (ContractCallFailed)
Watch result: false

Logs when tx is made non-owner of the Wallet:

~/Desktop/Agoric/agoric-notes> yarn watch
Starting to watch on Arbitrum Sepolia testnet...
subscribed: 0xcb6ee235e2a5269530888c4c111e13a1
errrior from call: Error: execution reverted (unknown custom error) (action="call", data="0xde6a3cbf0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c377100000000000000000000000000000000000000", reason=null, transaction={ "data": "0x49160658a79ce2ad28ae604f0f2474e4407079866e7e9fbccb68f4ea9db25188c7467752000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000661676f7269630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c37710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747835350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000927c000000000000000000000000000000000000000000000000000000000000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000084617ba03700000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000927c00000000000000000000000007a4150a9bf3bee704493b7178c35b7c174b4f6ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "from": "0xD36aac0c9676e984D72823Fb662ce94D3Ab5E551", "to": "0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.13.4)
    at makeError (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/utils/errors.ts:694:21)
    at getBuiltinCallException (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/abi/abi-coder.ts:118:12)
    at Function.getBuiltinCallException (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/abi/abi-coder.ts:235:16)
    at WebSocketProvider.getRpcError (file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:989:32)
    at file:///home/rabi/Desktop/Agoric/agoric-notes/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:563:45 {
  code: 'CALL_EXCEPTION',
  action: 'call',
  data: '0xde6a3cbf0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c377100000000000000000000000000000000000000',
  reason: null,
  transaction: {
    to: '0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF',
    data: '0x49160658a79ce2ad28ae604f0f2474e4407079866e7e9fbccb68f4ea9db25188c7467752000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000661676f7269630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c37710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747835350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000927c000000000000000000000000000000000000000000000000000000000000000000000000000000000bfc91d59fdaa134a4ed45f7b584caf96d7792eff00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000084617ba03700000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d00000000000000000000000000000000000000000000000000000000000927c00000000000000000000000007a4150a9bf3bee704493b7178c35b7c174b4f6ff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    from: '0xD36aac0c9676e984D72823Fb662ce94D3Ab5E551'
  },
  invocation: null,
  revert: null,
  shortMessage: 'execution reverted (unknown custom error)',
  info: {
    error: {
      code: 3,
      message: 'execution reverted',
      data: '0xde6a3cbf0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c377100000000000000000000000000000000000000'
    },
    payload: { method: 'eth_call', params: [Array], id: 4, jsonrpc: '2.0' }
  }
}
revertData: 0xde6a3cbf0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002d61676f726963316565396872306a7972786879393939793735356d703836326c6a6779636d77797034706c377100000000000000000000000000000000000000
⚠️  IGNORED: txId=tx55 txHash=0xaaa49b1a63ec7d18e10dd2fa0befc682c3c51033962f0b903f73e34d4f8f20e4 block=231637688 (non-owner or other revert: 0xde6a3cbf)

I’ve made many updates since the last round of testing, so I will need to do E2E testing again.

@rabi-siddique rabi-siddique marked this pull request as ready for review January 8, 2026 12:23
@rabi-siddique rabi-siddique force-pushed the rs-find-reverted-transactions branch from 5bfdeab to 51dd6d0 Compare January 12, 2026 10:32
@rabi-siddique rabi-siddique changed the base branch from master to rs-publish-src-addr January 12, 2026 10:34
@rabi-siddique rabi-siddique force-pushed the rs-find-reverted-transactions branch from b9fde86 to b8b1099 Compare January 12, 2026 12:51
@rabi-siddique
Copy link
Contributor Author

rabi-siddique commented Jan 12, 2026

E2E testing results

Success Case

Processing pendingTx event published.vStoragePusher.portfolios.tx6
New pending tx {
  txId: 'tx6',
  destinationAddress: 'eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF',
  sourceAddress: 'cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk',
  status: 'pending',
  type: 'GMP'
}
[tx6] handling GMP tx
{
  blockHeight: 5088146n,
  portfolioEvents: [],
  pendingTxEvents: [
    {
      path: 'published.vStoragePusher.portfolios.tx6',
      value: '{"blockHeight":"5088146","values":["{\\"body\\":\\"#{\\\\\\"destinationAddress\\\\\\":\\\\\\"eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF\\\\\\",\\\\\\"sourceAddress\\\\\\":\\\\\\"cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk\\\\\\",\\\\\\"status\\\\\\":\\\\\\"pending\\\\\\",\\\\\\"type\\\\\\":\\\\\\"GMP\\\\\\"}\\",\\"slots\\":[]}"]}',
      eventRecord: {
        blockHeight: 5088146n,
        type: 'kvstore',
        event: { type: 'state_change', attributes: [Array] }
      }
    }
  ]
}
[tx6] Watching transaction status for txId: tx6 at contract: 0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF
  • Tx Resolved:
[tx6] ✅ SUCCESS: txId=tx6 txHash=0x71661be709ec9c3cffd010f49602cf6e0acba9d520906ea423fb3ac3cf9ae168 block=232827903
{
  maxRetries: 6,
  retryIntervalMs: 3500,
  message: 'offer-1768223624201'
}

Revert Case(Authorized User)

  • User tries to deploy more funds than they have: AxelarScan Link

  • Resolver Logs:

Processing pendingTx event published.vStoragePusher.portfolios.tx7
New pending tx {
  txId: 'tx7',
  destinationAddress: 'eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF',
  sourceAddress: 'cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk',
  status: 'pending',
  type: 'GMP'
}
[tx7] handling GMP tx
{
  blockHeight: 5088193n,
  portfolioEvents: [],
  pendingTxEvents: [
    {
      path: 'published.vStoragePusher.portfolios.tx7',
      value: '{"blockHeight":"5088193","values":["{\\"body\\":\\"#{\\\\\\"destinationAddress\\\\\\":\\\\\\"eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF\\\\\\",\\\\\\"sourceAddress\\\\\\":\\\\\\"cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk\\\\\\",\\\\\\"status\\\\\\":\\\\\\"pending\\\\\\",\\\\\\"type\\\\\\":\\\\\\"GMP\\\\\\"}\\",\\"slots\\":[]}"]}',
      eventRecord: {
        blockHeight: 5088193n,
        type: 'kvstore',
        event: { type: 'state_change', attributes: [Array] }
      }
    }
  ]
}
[tx7] Watching transaction status for txId: tx7 at contract: 0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF
  • Revert detected and resolved:
[tx7] ❌ REVERTED: txId=tx7 txHash=0x88433f939abf53d4a546caf9789a6bda21d7d55f105f38b3b5f177940086b7ca block=232829871 - transaction failed
{ blockHeight: 5088281n, portfolioEvents: [], pendingTxEvents: [] }
{
  maxRetries: 6,
  retryIntervalMs: 3500,
  message: 'offer-1768224223328'
}

mergify bot added a commit that referenced this pull request Jan 12, 2026
…12327)

closes:
- https://github.com/Agoric/agoric-private/issues/699

## Description


Publishes the expected `sourceAddress` (LCA) for pending GMP transactions into vstorage and updates snapshots accordingly.

This enables the GMP watcher to reliably classify reverted Wallet executions by comparing the decoded `sourceAddress` from `calldata` against the expected value, avoiding revert simulation and state-drift issues(see #12311).

### Testing Considerations

The vstorage snapshot tests are enough to verify this change.

### Upgrade Considerations

- Will require a `ymax0` and `ymax1` update on mainnet.
Base automatically changed from rs-publish-src-addr to master January 12, 2026 13:31
@github-actions
Copy link

Base branch is changed to master. Please re-run the integration tests by adding 'force:integration' label.

@rabi-siddique
Copy link
Contributor Author

Revert Case(Non-Authorized)

  • Non-Owner makes a GMP call: AxelarScan Link
    Wallet Owner: agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk
    User making the call: agoric1ee9hr0jyrxhy999y755mp862ljgycmwyp4pl7q

  • Resolver logs:

Processing pendingTx event published.vStoragePusher.portfolios.tx8
New pending tx {
  txId: 'tx8',
  destinationAddress: 'eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF',
  sourceAddress: 'cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk',
  status: 'pending',
  type: 'GMP'
}
[tx8] handling GMP tx
{
  blockHeight: 5088334n,
  portfolioEvents: [],
  pendingTxEvents: [
    {
      path: 'published.vStoragePusher.portfolios.tx8',
      value: '{"blockHeight":"5088334","values":["{\\"body\\":\\"#{\\\\\\"destinationAddress\\\\\\":\\\\\\"eip155:421614:0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF\\\\\\",\\\\\\"sourceAddress\\\\\\":\\\\\\"cosmos:agoricdev-25:agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk\\\\\\",\\\\\\"status\\\\\\":\\\\\\"pending\\\\\\",\\\\\\"type\\\\\\":\\\\\\"GMP\\\\\\"}\\",\\"slots\\":[]}"]}',
      eventRecord: {
        blockHeight: 5088334n,
        type: 'kvstore',
        event: { type: 'state_change', attributes: [Array] }
      }
    }
  ]
}
[tx8] Watching transaction status for txId: tx8 at contract: 0x7a4150a9BF3bEE704493B7178c35b7C174b4F6fF
  • Tx Ignored and not resolved:
[tx8] ⚠️  IGNORED: txId=tx8 txHash=0xe791575a559163f688012dcc61cb0780e2ce7a3c2ceff3d7f6ceaa551b22960d - sourceAddress mismatch (expected agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk, got agoric1ee9hr0jyrxhy999y755mp862ljgycmwyp4pl7q)
{ blockHeight: 5088440n, portfolioEvents: [], pendingTxEvents: [] }
{ blockHeight: 5088441n, portfolioEvents: [], pendingTxEvents: [] }

[tx8] ✅ SUCCESS: txId=tx8 txHash=0xc2bffa5df7d84798ed0bf0490a73cef0330260c94d28c7f6c68559ffa7c3c476 block=232832927
{
  maxRetries: 6,
  retryIntervalMs: 3500,
  message: 'offer-1768225192767'
}

@rabi-siddique rabi-siddique force-pushed the rs-find-reverted-transactions branch from b8b1099 to 183e078 Compare January 12, 2026 13:41
const provider = ctx.evmProviders[caipId] as WebSocketProvider;

// Extract the address portion from sourceAddress (format: 'cosmos:agoric-3:agoric1...')
const lcaAddress = parseAccountId(sourceAddress).accountAddress;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to assert for lcaAddress presence/correctness? What if accountAddress is not the required format? Or does parseAccountId handle that from inside already (as hard failure)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is required and validated in engine.ts via mustMatch

Comment on lines +269 to +273
if (result.settled) {
const reason = `${logPrefix} Live mode completed`;
log(reason);
abortController.abort(reason);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about handling tx not settled here?

Comment on lines +216 to +223
return { settled: false };
}

deleteTxBlockLowerBound(kvStore, txId);
return { found: true, txHash: matchingEvent.transactionHash };
return { settled: true, txHash: matchingEvent.transactionHash };
} catch (error) {
log(`Error:`, error);
return { found: false };
return { settled: false };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need some extra identifier to differentiate b/w not found and error? Because both says settled: false

let timeoutId: NodeJS.Timeout | undefined;
let subId: string | null = null;

const ws = provider.websocket as WebSocket;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this provider.websocket natively provided by alchemy/ethers, right? We don't need to manually connect to it I see?

Copy link
Contributor Author

@rabi-siddique rabi-siddique Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, provider.websocket is natively provided by ethers.js when using a WebSocketProvider

reject(WATCH_GMP_ABORTED);
return;
}
if (signal?.aborted) return resolve({ settled: false });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment why resolving instead of rejection now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, it's the same pattern in all watchers. We don't make use of reject. I think ideally we can refactor later in all watchers to reject with rejectionReason.

`Watching for MulticallStatus and MulticallExecuted events for txId: ${txId} at contract: ${contractAddress}`,
);
// Named so we can remove it
const onAbort = () => finish({ settled: false });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we wanna explicitly log when we aborted signal? Might help in debugging.

Comment on lines +190 to +193
ws.off('message', messageHandler);
ws.off('error', onWsError);
ws.off('close', onWsClose);
signal?.removeEventListener('abort', onAbort);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a log say: "Cleaning up ws and listeners"..?

Comment on lines +306 to +316
const subscribe = async () => {
await provider.getNetwork();

subId = await provider.send('eth_subscribe', [
'alchemy_minedTransactions',
{
addresses: [{ to: contractAddress }],
includeRemoved: false,
hashesOnly: false,
},
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we handle the failure to subscribe here? Or does the caller handle that in catch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's happening via .catch(fail) on line 327

Comment on lines +334 to +338
if (!done) {
log(
`✗ No MulticallStatus or MulticallExecuted found for txId ${txId} within ${timeoutMs / 60000} minutes`,
);
const { status, errorMessage } = await findTxStatusFromAxelarscan(
txId,
contractAddress,
{
axelarApiUrl,
fetch,
},
`✗ No transaction status found for txId ${txId} within ${
timeoutMs / 60000
} minutes`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we make done = true on failure as well. So done essentially means that tx is processed? Either success/failed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, done means the watcher is finished (stopped watching), regardless of whether the transaction succeeded, failed, or was never observed.


// Custom error code for aborted GMP watch
export const WATCH_GMP_ABORTED = 'WATCH_GMP_ABORTED';
// Alchemy alchemy_minedTransactions subscription message types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link to documentation if possible, and feel free to use //#region///#endregion.

fetch: typeof fetch;
axelarApiUrl: string;
};
// AxelarExecutable entrypoint (standard)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link to documentation if possible.

Comment on lines +69 to +71
const WALLET_EXECUTE_ABI = [
'function execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload) external',
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GMP_ABI uses the same _ABI name suffix but the JSON ABI representation, and I think particularly the inputs of a function... can we commit to and document a particular convention? I wouldn't mind if it's verbose, e.g. GMP_INPUTS_ABI_JSON and WALLET_EXECUTE_CONTRACT_ABI_TEXT.

Comment on lines +88 to +91
// execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload)
const sourceAddress: string = parsed.args?.[2];
const payload: string = parsed.args?.[3];
if (!sourceAddress || !payload) return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload)
const sourceAddress: string = parsed.args?.[2];
const payload: string = parsed.args?.[3];
if (!sourceAddress || !payload) return null;
const [_commandId, _sourceChain, sourceAddress, payload] = parsed.args;
if (!sourceAddress || !payload) return null;

Comment on lines +93 to +100
// CallMessage { string id; ContractCalls[] calls; }
const decoded = abiCoder.decode(
['tuple(string id, tuple(address target, bytes data)[] calls)'],
payload,
);

const txId = decoded?.[0]?.id;
if (typeof txId !== 'string') return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// CallMessage { string id; ContractCalls[] calls; }
const decoded = abiCoder.decode(
['tuple(string id, tuple(address target, bytes data)[] calls)'],
payload,
);
const txId = decoded?.[0]?.id;
if (typeof txId !== 'string') return null;
// CallMessage { string id; ContractCalls[] calls; }
const [decoded] = abiCoder.decode(
['tuple(string id, tuple(address target, bytes data)[] calls)'],
payload,
);
const txId = decoded?.id;
if (typeof txId !== 'string') return null;

Comment on lines +276 to +297
if (receipt.status === 1 && matchingLog) {
log(
`✅ SUCCESS: txId=${txId} txHash=${txHash} block=${receipt.blockNumber}`,
);
return finish({ settled: true, txHash });
}

if (receipt.status === 0) {
/**
* Transaction reverted - since we've already validated that the sourceAddress
* matches our expected LCA address, this is a legitimate execution attempt
* from our own wallet that failed. We treat this as a transaction failure.
*
* Note: Spurious executions from unauthorized parties are already filtered
* out by the sourceAddress check above, so any revert we see here represents
* a genuine failure of the user's operation.
*/
log(
`❌ REVERTED: txId=${txId} txHash=${txHash} block=${receipt.blockNumber} - transaction failed`,
);
return finish({ settled: true, txHash });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it troubling that these two cases collapse into indistinguishable output.

Comment on lines +309 to +318
subId = await provider.send('eth_subscribe', [
'alchemy_minedTransactions',
{
addresses: [{ to: contractAddress }],
includeRemoved: false,
hashesOnly: false,
},
]);

ws.on('message', messageHandler);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance of a race here? Our general pattern is to register listeners before creating circumstances that could cause them to be invoked.

Comment on lines +321 to +324
// Attach listeners (all removable)
ws.on('error', onWsError);
ws.on('close', onWsClose);
signal?.addEventListener('abort', onAbort);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here.

Comment on lines 345 to 347
export const EVENTS = {
MULTICALL_EXECUTED: 'executed',
MULTICALL_STATUS: 'status',
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the EVENTS container is now superfluous.


// Mock websocket for gmp-watcher - store event handlers
const wsEventHandlers = new Map<string, Function[]>();
const mockWebSocket = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be hand-rolling a partial implementation of EventEmitter; why not just use what's already provided?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants