Skip to content

fix: support Safe wallets in OVM/splitter deployment flows#226

Merged
HananINouman merged 1 commit into
mainfrom
Hanan/safe-tx-receipt-resolution
Jul 10, 2026
Merged

fix: support Safe wallets in OVM/splitter deployment flows#226
HananINouman merged 1 commit into
mainfrom
Hanan/safe-tx-receipt-resolution

Conversation

@HananINouman

@HananINouman HananINouman commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Safe wallets over WalletConnect return the internal safeTxHash instead of an on-chain transaction hash, so ethers' sendTransaction polling and tx.wait() hang forever and createValidatorManagerAndRewardsSplit / createValidatorManagerAndTotalSplit never resolve, even though the deployment executes on-chain.

  • Add executeTransactionSafeAware: EOAs keep the standard sendTransaction + wait path; contract wallets submit via sendUncheckedTransaction and poll for either a direct receipt or the Safe's ExecutionSuccess event carrying the safeTxHash (v1.3.0 and v1.4.1 layouts), resolving the real transaction receipt. ExecutionFailure surfaces as an error.
  • Use it in deployOVMContract and multicall3, covering both the rewards-only and split-everything flows.
  • Extract the OVM address by parsing receipt logs for the factory's CreateObolValidatorManager event instead of positional log indexing, which broke when Safe execution events were interleaved.

Summary by CodeRabbit

  • New Features

    • Improved transaction handling for both standard wallets and smart-contract wallets, including safer submission and more reliable receipt detection.
    • Added more robust deployment and multicall flows that better surface the final on-chain result.
  • Bug Fixes

    • Fixed receipt lookup for wallet-based transactions so confirmations are found more reliably across wallet versions.
    • Improved error handling for failed wallet executions and timeout cases.
    • Made address extraction from deployment events more dependable.

@HananINouman HananINouman requested a review from a team as a code owner July 9, 2026 09:32
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@HananINouman, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72223459-2dad-489c-80b8-216d0e2907e3

📥 Commits

Reviewing files that changed from the base of the PR and between 297e8aa and bc45fed.

📒 Files selected for processing (2)
  • src/splits/splitHelpers.ts
  • test/splits/ovmDeployHelpers.spec.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Hanan/safe-tx-receipt-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/splits/safeWalletHelpers.spec.ts (2)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider removing @ts-nocheck in favor of targeted type casts.

@ts-nocheck disables all TypeScript checking for the file, which can hide genuine type errors in test setup. Consider using as unknown as SignerType casts on mock objects instead, preserving type safety for the rest of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/splits/safeWalletHelpers.spec.ts` at line 1, The test file is
suppressing all TypeScript checks with `@ts-nocheck`, which hides real issues in
the setup. Remove the file-wide suppression and use targeted casts on the mock
signer objects instead, such as casting to SignerType via unknown where needed,
so the rest of safeWalletHelpers.spec.ts remains type-checked.

50-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for timeout and sendUncheckedTransaction fallback paths.

The upstream executeTransactionSafeAware has two untested branches:

  1. Timeout path — when no receipt or Safe event is found within SAFE_TX_TIMEOUT_MS, it throws 'Timed out waiting for the Safe transaction to be executed...'. This is the primary user-facing error when a Safe tx isn't executed yet, so it should be covered.

  2. sendUncheckedTransaction fallback — when the signer doesn't expose sendUncheckedTransaction, the code falls back to signer.sendTransaction and uses .hash. This path is untested.

  3. Reverted direct receipt — when getTransactionReceipt returns a receipt with status === 0, the code throws 'Transaction failed or was reverted'. Not tested.

🧪 Suggested additional tests
it('Safe: throws on timeout when no receipt or event is found', async () => {
  const provider = makeProvider({
    getCode: jest.fn().mockResolvedValue('0x6080'),
  });
  const signer = makeSigner(provider, {
    sendUncheckedTransaction: jest.fn().mockResolvedValue(SAFE_TX_HASH),
  });

  await expect(
    executeTransactionSafeAware({ signer, ...txArgs }),
  ).rejects.toThrow('Timed out waiting for the Safe transaction');
});

it('Safe: falls back to sendTransaction when sendUncheckedTransaction is unavailable', async () => {
  const realReceipt = { status: 1, hash: REAL_TX_HASH };
  const provider = makeProvider({
    getCode: jest.fn().mockResolvedValue('0x6080'),
    getTransactionReceipt: jest.fn().mockResolvedValue(realReceipt),
  });
  const signer = makeSigner(provider, {
    sendTransaction: jest.fn().mockResolvedValue({ hash: REAL_TX_HASH }),
  });

  const result = await executeTransactionSafeAware({ signer, ...txArgs });
  expect(result).toBe(realReceipt);
  expect(signer.sendTransaction).toHaveBeenCalledWith(txArgs);
});

it('Safe: throws when direct receipt has status 0 (reverted)', async () => {
  const provider = makeProvider({
    getCode: jest.fn().mockResolvedValue('0x6080'),
    getTransactionReceipt: jest.fn().mockResolvedValue({ status: 0, hash: REAL_TX_HASH }),
  });
  const signer = makeSigner(provider, {
    sendUncheckedTransaction: jest.fn().mockResolvedValue(REAL_TX_HASH),
  });

  await expect(
    executeTransactionSafeAware({ signer, ...txArgs }),
  ).rejects.toThrow('Transaction failed or was reverted');
});

Note: the timeout test may need jest.useFakeTimers() or a reduced SAFE_TX_TIMEOUT_MS to avoid a long-running test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/splits/safeWalletHelpers.spec.ts` around lines 50 - 163, Add tests in
executeTransactionSafeAware to cover the missing branches: the Safe timeout path
when no receipt/event is found within SAFE_TX_TIMEOUT_MS, the
sendUncheckedTransaction fallback to signer.sendTransaction().hash when
unchecked sending is unavailable, and the reverted direct-receipt case where
getTransactionReceipt returns status 0. Use the existing
executeTransactionSafeAware, makeProvider, and makeSigner helpers in
safeWalletHelpers.spec.ts, and assert the specific error messages and fallback
method calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/splits/safeWalletHelpers.spec.ts`:
- Line 1: The test file is suppressing all TypeScript checks with `@ts-nocheck`,
which hides real issues in the setup. Remove the file-wide suppression and use
targeted casts on the mock signer objects instead, such as casting to SignerType
via unknown where needed, so the rest of safeWalletHelpers.spec.ts remains
type-checked.
- Around line 50-163: Add tests in executeTransactionSafeAware to cover the
missing branches: the Safe timeout path when no receipt/event is found within
SAFE_TX_TIMEOUT_MS, the sendUncheckedTransaction fallback to
signer.sendTransaction().hash when unchecked sending is unavailable, and the
reverted direct-receipt case where getTransactionReceipt returns status 0. Use
the existing executeTransactionSafeAware, makeProvider, and makeSigner helpers
in safeWalletHelpers.spec.ts, and assert the specific error messages and
fallback method calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6dfe2975-da5b-494f-95ff-525b0499306d

📥 Commits

Reviewing files that changed from the base of the PR and between 531ced4 and 297e8aa.

📒 Files selected for processing (3)
  • src/splits/safeWalletHelpers.ts
  • src/splits/splitHelpers.ts
  • test/splits/safeWalletHelpers.spec.ts

@HananINouman HananINouman force-pushed the Hanan/safe-tx-receipt-resolution branch 2 times, most recently from 6af042b to 9c07cc5 Compare July 9, 2026 11:07
Safe wallets over WalletConnect return the internal safeTxHash instead of an
on-chain transaction hash, so ethers' sendTransaction polling and tx.wait()
hang forever and createValidatorManagerAndRewardsSplit /
createValidatorManagerAndTotalSplit never resolve, even though the deployment
executes on-chain.

For contract-wallet signers (detected with the existing isContractAvailable),
submit via sendUncheckedTransaction and resolve the OVM address from the
factory's CreateObolValidatorManager events for the owner, accepting a
candidate only when its transaction is provably ours: its hash equals the
wallet-returned hash, or its receipt carries our Safe's
ExecutionSuccess(safeTxHash). This makes concurrent or third-party OVM
creations for the same owner unmistakable for ours (the factory is
permissionless). A backfill query covers executions mined before the wallet
call returned. Covers both the rewards-only and split-everything flows; EOA
signers keep the unchanged sendTransaction + wait path.

Also extract the OVM address from receipts by parsing logs for the factory
event instead of positional log indexing, which broke when extra events were
interleaved in a transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HananINouman HananINouman force-pushed the Hanan/safe-tx-receipt-resolution branch from 9c07cc5 to bc45fed Compare July 9, 2026 11:08
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@HananINouman HananINouman merged commit b7076e6 into main Jul 10, 2026
5 of 9 checks passed
@HananINouman HananINouman deleted the Hanan/safe-tx-receipt-resolution branch July 10, 2026 07:07
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.

2 participants