Skip to content

feat(radix-sdk, deploy-sdk, provider-sdk): mailbox validator artifact interfaces#8301

Merged
xeno097 merged 21 commits intomainfrom
andrey/mailbox-validator-artifact-interfaces
Mar 12, 2026
Merged

feat(radix-sdk, deploy-sdk, provider-sdk): mailbox validator artifact interfaces#8301
xeno097 merged 21 commits intomainfrom
andrey/mailbox-validator-artifact-interfaces

Conversation

@xeno097
Copy link
Copy Markdown
Contributor

@xeno097 xeno097 commented Mar 6, 2026

Description

Added mailbox and validator announce artifact interfaces to provider-sdk, implemented Radix-specific artifact managers (reader/writer) in radix-sdk, and added a composite CoreArtifactReader in deploy-sdk that orchestrates mailbox

  • ISM + hook reading.

provider-sdk:

  • New mailbox.ts: MailboxConfig, DeployedMailboxArtifact, IRawMailboxArtifactManager, mailboxArtifactToDerivedCoreConfig converter
  • New validator-announce.ts: ValidatorAnnounceConfig, IRawValidatorAnnounceArtifactManager

radix-sdk:

  • RadixMailboxArtifactManager with reader (fetches on-chain config) and writer (create/update mailbox)
  • RadixValidatorAnnounceArtifactManager with reader and writer (create only, immutable)
  • E2e tests for both mailbox and validator announce artifacts

deploy-sdk:

  • CoreArtifactReader: composite reader that takes a mailbox address, reads raw config, then expands nested ISM/hook artifacts via existing readers. Provides backward-compatible deriveCoreConfig().

Drive-by changes

  • Moved ismArtifactToDerivedConfig from deploy-sdk generic-ism.ts to provider-sdk ism.ts (removed duplication after rebase)
  • RadixProvider.getMailbox now returns zero addresses instead of undefined for optional mailbox fields (defaultIsm, defaultHook, requiredHook)
  • provider-sdk mailbox query types (ResGetMailbox) updated to support optional fields with zero address fallback

Related issues

None

Backward compatibility

Yes — new interfaces and implementations only, no breaking changes to existing APIs. The ismArtifactToDerivedConfig move is re-exported from the same entry point.

Testing

E2e tests for Radix mailbox and validator announce artifact managers.

Summary by CodeRabbit

  • New Features

    • Mailbox artifact management: read, create, update mailbox configurations and conversion to core configs.
    • Validator announce artifact management: create immutable announces and read on-chain configs.
    • Composite core artifact reader to expand mailbox + ISM + hook artifacts.
    • Added isEmptyAddress helper and new provider exports for mailbox and validator-announce.
  • Tests

    • End-to-end tests for mailbox and validator-announce lifecycles.
  • Chores

    • Minor version bumps for related packages.

antigremlin and others added 11 commits March 6, 2026 11:56
…faces

- Define MailboxConfig with ISM and Hook artifact references
- Define ValidatorAnnounceConfig with mailbox address reference
- Add raw artifact variants for protocol implementation use
- Export new modules in package.json
- Follow existing ISM/Hook artifact patterns
- RadixMailboxArtifactManager implements IRawMailboxArtifactManager
- RadixMailboxReader reads mailbox config from chain (owner, ISM, hooks)
- RadixMailboxWriter supports create and update operations
- Create deploys mailbox and sets ISM/hooks via setters
- Update compares state and generates update transactions
- Exports RadixMailboxArtifactManager from index
- Test create mailbox with ISM and hooks
- Test read mailbox configuration from chain
- Test update mailbox when ISM changes
- Test update mailbox when owner changes
- Test no-op updates when state matches
- Test readMailbox convenience method
- Created validator-announce.ts combining query and tx functions
- RadixValidatorAnnounceArtifactManager implements IRawValidatorAnnounceArtifactManager
- RadixValidatorAnnounceReader reads mailbox address from chain
- RadixValidatorAnnounceWriter creates validator announce (immutable, no updates)
- Added comprehensive e2e tests for create, read, update (no-op), and convenience methods
- Exported RadixValidatorAnnounceArtifactManager from index
- Create test mailbox in before() hook instead of using burn address
- Validator announce requires valid mailbox contract address
- Fixes NoMethodMapping error when trying to call local_domain on Account
- All 5 validator announce e2e tests now passing
…reading

- Implements ArtifactReader<MailboxConfig, DeployedMailboxAddress>
- Orchestrates raw mailbox reader + ISM/hook readers to expand nested artifacts
- Provides deriveCoreConfig() for backward compatibility with AltVMCoreReader
- Moves mailboxArtifactToDerivedCoreConfig to provider-sdk/src/mailbox.ts
- Exports ismArtifactToDerivedConfig from deploy-sdk ISM reader
- Cleans up provider-sdk/src/core.ts to keep only legacy types
…h zero addresses

- Add ConfigOnChain<C> type to preserve nested artifact deployment types
- Rename RawMailboxConfig to MailboxOnChain for clarity
- Add getOptionalFieldValueFromEntityState helper for Option::None handling
- Skip setter transactions when zero addresses provided (optimization)
- Update RadixMailboxWriter to conditionally set ISM/hooks
- Add test coverage for zero address mailbox creation
- Normalize optional fields to zero address in RadixProvider
- Remove 'any' types from mailboxArtifactToDerivedCoreConfig converters
@github-project-automation github-project-automation Bot moved this to In Review in Hyperlane Tasks Mar 6, 2026
@xeno097 xeno097 marked this pull request as draft March 6, 2026 16:25
@xeno097 xeno097 changed the title feat(radix-sdk, deploy-sdk, provider-sdk)/mailbox validator artifact interfaces feat(radix-sdk, deploy-sdk, provider-sdk): mailbox validator artifact interfaces Mar 6, 2026
@xeno097 xeno097 marked this pull request as ready for review March 12, 2026 14:43
@xeno097 xeno097 requested a review from paulbalaji March 12, 2026 14:43
@xeno097
Copy link
Copy Markdown
Contributor Author

xeno097 commented Mar 12, 2026

@claude brutally honest review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 12, 2026

Claude finished @xeno097's task in 3m 14s —— View job


Brutally Honest Review

Alright, you asked for it. Overall code quality is solid, but there's unnecessary complexity creeping in.


🟢 The Good

Type safety: No as casts, no any types, no non-null assertions. Actually follows the TypeScript guidelines. This is rare and appreciated.

Test coverage: Comprehensive e2e tests for both mailbox and validator announce. Tests cover create, read, update, and edge cases like zero addresses.

Error handling: Proper use of assert() for preconditions. No defensive fallbacks masking bugs.

Backward compatibility: The deriveCoreConfig() method maintains compatibility with existing code.

Changesets: Well-written, past tense, descriptive. Follow the guidelines.


🟡 The Questionable

1. Premature abstraction for "future variants"

// mailbox.ts:54-64
export type MailboxArtifactConfig = MailboxConfig;  // Why?

export interface MailboxArtifactConfigs {
  mailbox: MailboxConfig;  // Only one variant
}

export type MailboxType = keyof MailboxArtifactConfigs;  // "mailbox" literal

You have ONE mailbox type. But you've built a discriminated union infrastructure "to support future variants." The CLAUDE.md explicitly warns against this:

"Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task."

Same issue in validator-announce.ts. If you need multiple variants later, add them later. Right now this is noise.

2. Thin wrapper methods that add no value

// RadixMailboxArtifactManager
async readMailbox(address: string) {
  const reader = this.createReader('mailbox');
  return reader.read(address);
}

This instantiates a reader just to call one method. Why not just:

async readMailbox(address: string) {
  return new RadixMailboxReader(this.gateway).read(address);
}

The createReader('mailbox') indirection with the type parameter is enterprise-style ceremony for no benefit.

3. Inconsistent file organization

Validator announce:

  • validator-announce.ts - combined query + tx functions
  • validator-announce-reader.ts, validator-announce-writer.ts - separate reader/writer
  • validator-announce-artifact-manager.ts

Mailbox:

  • mailbox.ts - combined reader + writer classes
  • mailbox-query.ts, mailbox-tx.ts - separate query/tx functions
  • mailbox-artifact-manager.ts

Why is validator announce's query+tx combined but mailbox's separated? Why is mailbox's reader+writer combined but validator announce's separated? Pick a pattern and stick with it.

4. Unused interface

// mailbox.ts:70-74
export type IMailboxArtifactManager = IArtifactManager<
  MailboxType,
  MailboxArtifactConfigs,
  DeployedMailboxAddress
>;

This interface is defined but never implemented or used. It's just sitting there "for consistency." Delete it or use it.

5. The "Raw" vs "Compound" type distinction

I get it - Raw = addresses for nested artifacts, Compound = fully expanded configs. But look at the type machinery this creates:

  • MailboxConfig vs MailboxOnChain
  • RawMailboxArtifactConfigs vs MailboxArtifactConfigs
  • IRawMailboxArtifactManager vs IMailboxArtifactManager
  • DeployedRawMailboxArtifact vs DeployedMailboxArtifact

For a codebase that values simplicity and explicitly warns against "enterprise-style patterns," this is a lot of abstraction machinery. Is the complexity justified?


🔴 The Bad

Zero address handling is a breaking change

PR description says this is a "drive-by change":

RadixProvider.getMailbox now returns zero addresses instead of undefined for optional mailbox fields

This could break existing code that checks:

if (mailbox.defaultIsm !== undefined) { ... }

Now it's always defined (as zero address). Did you verify nothing breaks? This should be in the "Breaking changes" section, not "Drive-by changes."

typescript/radix-sdk/src/clients/provider.ts:195-202

Misleading PR description

You say you "moved ismArtifactToDerivedConfig from deploy-sdk to provider-sdk" but don't mention this was deduplication - it existed identically in both places. The git diff shows you removed 48 lines of duplicated code from generic-ism.ts. That's good! But you buried the lede - this should be highlighted as reducing duplication.


🤔 The Philosophical Question

Looking at the broader artifact API pattern across this PR and the codebase... is this over-engineered?

The artifact state machine: NEWUNDERIVEDDEPLOYED
The generic type parameters: IArtifactManager<TType, TConfigs, TDeployed>
The discriminated unions: Artifact<TConfig, TDeployed> with state-based narrowing
The Raw vs Compound split: Separate type hierarchies for unexpanded vs expanded

This feels like bringing Java/C# enterprise patterns to TypeScript. The CLAUDE.md says:

"Avoid enterprise-style patterns: No deep inheritance hierarchies; prefer composition. No over-engineered abstractions for 'future-proofing'."

I'm not saying scrap it - you've implemented the existing pattern consistently. But the pattern itself might not align with the stated philosophy. Worth discussing with the team.


📝 Minor Nitpicks

  1. Magic test domain IDs: 999999 and 999998 - why these? Add a comment or constant.

  2. Redundant comments:

    // Create the mailbox (mailbox is created with signer as initial owner)
    const createTx = await getCreateMailboxTx(...)

    Says the same thing as the code.

  3. Type alias noise:

    export type MailboxArtifactConfig = MailboxConfig;

    What does this buy you? Just use MailboxConfig.


✅ Verdict

Code quality: 8/10 - Solid TypeScript, good tests, no type safety issues.
Architecture alignment: 5/10 - Adds complexity that conflicts with stated simplicity goals.
Breaking changes: Needs verification - Zero address change could break things.

Recommendation: Merge with caution. Verify the zero address change doesn't break anything. Consider simplifying the type machinery in future iterations.


@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d88f184-dc2e-46ff-8d3c-9a0c9262aed2

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb9702 and b8ca2ef.

📒 Files selected for processing (1)
  • typescript/provider-sdk/src/mailbox.ts

📝 Walkthrough

Walkthrough

This PR adds mailbox and validator-announce artifact types, readers/writers, Radix artifact managers, a composite CoreArtifactReader, optional-field helpers, and an isEmptyAddress utility across provider-sdk, deploy-sdk, radix-sdk, and utils. It also adds end-to-end tests and package exports for the new modules.

Changes

Cohort / File(s) Summary
Changesets
.changeset/mailbox-validator-announce-artifacts.md, .changeset/orange-socks-return.md, .changeset/radix-mailbox-artifacts.md, .changeset/radix-validator-announce-artifacts.md, .changeset/utils-is-empty-address.md
Changelog entries for minor bumps and new features across packages.
Provider SDK — Types & Converters
typescript/provider-sdk/src/mailbox.ts, typescript/provider-sdk/src/validator-announce.ts, typescript/provider-sdk/package.json
New mailbox and validator-announce artifact types/interfaces, raw variants, mailboxArtifactToDerivedCoreConfig helper, and package exports for ./mailbox and ./validator-announce.
Deploy SDK — Composite Reader
typescript/deploy-sdk/src/core/core-artifact-reader.ts, typescript/deploy-sdk/src/index.ts, typescript/deploy-sdk/src/ism/generic-ism.ts
Adds CoreArtifactReader that expands mailbox → ISM/hook artifacts; deriveCoreConfig shim; delegates ISM derived conversion to ismArtifactToDerivedConfig.
Radix SDK — Mailbox mgmt & queries
typescript/radix-sdk/src/mailbox/..., typescript/radix-sdk/src/mailbox/mailbox-query.ts, typescript/radix-sdk/src/mailbox/mailbox-artifact-manager.ts
Radix mailbox reader/writer and artifact manager implementing raw mailbox flows (read/create/update), mailbox-query changes to optional fields, and E2E tests covering lifecycle and zero-address handling.
Radix SDK — Validator-announce mgmt & queries
typescript/radix-sdk/src/validator-announce/...
Adds Radix validator-announce reader/writer and artifact manager, query helper, immutable-create semantics, and E2E tests.
Radix SDK — Optional field helper
typescript/radix-sdk/src/utils/base-query.ts
New getOptionalFieldValueFromEntityState to read optional entity fields without throwing.
Radix SDK — Provider adjust
typescript/radix-sdk/src/clients/provider.ts
RadixProvider.getMailbox now fills mailbox optional fields with zero-address default when undefined.
Utils — Address helper
typescript/utils/src/addresses.ts, typescript/utils/src/index.ts
Adds and re-exports isEmptyAddress to treat undefined/null/zero-like addresses as empty.
Radix SDK — Public exports
typescript/radix-sdk/src/index.ts
Exports RadixMailboxArtifactManager and RadixValidatorAnnounceArtifactManager.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant CoreReader as CoreArtifactReader
participant RawMailboxMgr as IRawMailboxArtifactManager
participant ISMReader as IsmReader
participant HookReader as HookReader
participant Chain as Chain/Gateway

CoreReader->>RawMailboxMgr: readRawMailbox(address)
RawMailboxMgr-->>CoreReader: raw mailbox artifact (with ISM/hook refs)
CoreReader->>ISMReader: read(defaultIsmRef)
CoreReader->>HookReader: read(defaultHookRef), read(requiredHookRef)
ISMReader-->>CoreReader: deployed ISM artifact
HookReader-->>CoreReader: deployed hook artifacts
CoreReader->>CoreReader: assemble DeployedMailboxArtifact (DEPLOYED)
CoreReader-->>Client: return expanded mailbox artifact / DerivedCoreConfig

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • yorhodes
  • yjamin
  • troykessler

Poem

A swamp of types and readers, tidy as can be,
Mailboxes, validators sing “read, write, and flee,”
Core reader stitches pieces, plug'n'play with care,
Zero-address weeds pulled, artifacts laid bare,
Quietly the infra grows, steady as a tree. 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly identifies the main change: introducing mailbox and validator announce artifact interfaces across three packages (radix-sdk, deploy-sdk, provider-sdk).
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required sections: what's included, drive-by changes, backward compatibility, and testing approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch andrey/mailbox-validator-artifact-interfaces
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
.changeset/mailbox-validator-announce-artifacts.md (1)

5-5: Use past tense in changeset description, would ya?

The guidelines say changesets should describe what changed in past tense. "establish" should be "established".

📝 Suggested fix
-Added mailbox and validator announce artifact interfaces in provider-sdk. The new interfaces establish the contract for mailbox and validator announce artifacts, including MailboxConfig with ISM and Hook artifact references, ValidatorAnnounceConfig with mailbox address reference, and raw artifact variants for protocol implementation use.
+Added mailbox and validator announce artifact interfaces in provider-sdk. The new interfaces established the contract for mailbox and validator announce artifacts, including MailboxConfig with ISM and Hook artifact references, ValidatorAnnounceConfig with mailbox address reference, and raw artifact variants for protocol implementation use.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/mailbox-validator-announce-artifacts.md at line 5, Update the
changeset description to use past tense: change "establish" to "established" and
adjust any related verbs (e.g., "include" -> "included") so the sentence reads
that the new interfaces established the contract for mailbox and validator
announce artifacts, mentioning MailboxConfig, ValidatorAnnounceConfig, ISM, and
Hook artifact references in past tense.
typescript/radix-sdk/src/mailbox/mailbox.ts (1)

113-125: Inconsistent comparison pattern here - let me tell ya, consistency is like swamp water, it flows better when it's all the same.

Line 114 uses direct !== comparison for the ISM address, but hooks at lines 128-134 and 149-155 use eqOptionalAddress(). For Radix addresses, the latter is more robust. Keep it consistent.

♻️ Suggested fix
     // Set default ISM (only if not zero address)
-    if (defaultIsmAddress !== ZERO_ADDRESS_HEX_32) {
+    if (
+      !eqOptionalAddress(
+        defaultIsmAddress,
+        ZERO_ADDRESS_HEX_32,
+        eqAddressRadix,
+      )
+    ) {
       const setIsmTx = await getSetMailboxDefaultIsmTx(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/radix-sdk/src/mailbox/mailbox.ts` around lines 113 - 125, The code
currently compares defaultIsmAddress to ZERO_ADDRESS_HEX_32 using !==; change
this to use the Radix address helper for consistency and robustness by replacing
the check with a call to eqOptionalAddress (e.g., if
(!eqOptionalAddress(defaultIsmAddress, ZERO_ADDRESS_HEX_32))) before building
and broadcasting the set-ISM transaction (getSetMailboxDefaultIsmTx /
this.signer.signAndBroadcast); this aligns with the other checks in the file and
prevents subtle address-comparison issues.
.changeset/radix-validator-announce-artifacts.md (1)

5-5: Mixed tenses in this here changeset.

The description mixes past tense ("Implemented", "Combined") with present tense ("provides", "fetches", "creates"). Per coding guidelines, changeset descriptions should use past tense throughout describing what changed.

📜 Suggested past-tense revision
-Implemented validator announce artifact manager, reader, and writer for Radix SDK. Combined query and transaction functions in a unified validator-announce.ts file. RadixValidatorAnnounceArtifactManager provides read and create capabilities for validator announce deployments. The reader fetches the mailbox address from chain, and the writer creates new validator announce contracts (immutable, no update operations). Includes comprehensive e2e test coverage.
+Implemented validator announce artifact manager, reader, and writer for Radix SDK. Combined query and transaction functions in a unified validator-announce.ts file. RadixValidatorAnnounceArtifactManager provided read and create capabilities for validator announce deployments. The reader fetched the mailbox address from chain, and the writer created new validator announce contracts (immutable, no update operations). Included comprehensive e2e test coverage.

As per coding guidelines: "Write changeset descriptions in past tense describing what changed, not imperative form"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/radix-validator-announce-artifacts.md at line 5, The changeset
message uses mixed tenses; update the description to past tense throughout
(e.g., change "provides", "fetches", "creates" to "provided", "fetched",
"created") and make the verbs consistent with the opening past-tense verbs
("Implemented", "Combined"); specifically revise the lines mentioning
RadixValidatorAnnounceArtifactManager, validator-announce.ts, the reader and the
writer so the description uniformly describes what was changed in past tense.
typescript/deploy-sdk/src/core/core-artifact-reader.ts (1)

18-20: Import ordering is a wee bit swampy.

External package imports typically come before local imports, but ismArtifactToDerivedConfig from @hyperlane-xyz/provider-sdk/ism is placed after local imports. Move it up to join its kin.

🧅 Suggested reorder
-import { HookReader, createHookReader } from '../hook/hook-reader.js';
-import { IsmReader, createIsmReader } from '../ism/generic-ism.js';
-import { ismArtifactToDerivedConfig } from '@hyperlane-xyz/provider-sdk/ism';
+import { ismArtifactToDerivedConfig } from '@hyperlane-xyz/provider-sdk/ism';
+
+import { HookReader, createHookReader } from '../hook/hook-reader.js';
+import { IsmReader, createIsmReader } from '../ism/generic-ism.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/deploy-sdk/src/core/core-artifact-reader.ts` around lines 18 - 20,
Move the external import ismArtifactToDerivedConfig from
`@hyperlane-xyz/provider-sdk/ism` so it appears with the other external package
imports (before the local imports HookReader/createHookReader and
IsmReader/createIsmReader); specifically, relocate the
ismArtifactToDerivedConfig import line to the top import group so external
imports are grouped together and local imports follow.
typescript/radix-sdk/src/tests/mailbox.e2e-test.ts (1)

48-49: Reaching into private swamp territory with as any.

Accessing gateway and base via (radixSigner as any) breaks encapsulation and violates the guideline against as any casts. If these are needed for artifact manager construction, consider either:

  1. Exposing public getters on RadixSigner
  2. Having the artifact manager accept RadixSigner directly and extract what it needs internally

As per coding guidelines: "Do not use as any in TypeScript; use unknown with type guards if type is truly unknown"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/radix-sdk/src/tests/mailbox.e2e-test.ts` around lines 48 - 49, The
test is reaching into RadixSigner internals via (radixSigner as any).gateway and
.base which breaks encapsulation; instead either add public getters on
RadixSigner (e.g., getGateway(), getBase()) and use those in
mailbox.e2e-test.ts, or change the artifact manager constructor to accept a
RadixSigner instance (e.g., new ArtifactManager(radixSigner)) and let
ArtifactManager extract gateway/base internally; if the type really is
uncertain, use unknown with a type guard that narrows to an interface exposing
gateway/base rather than using as any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/orange-socks-return.md:
- Line 6: Rewrite the changeset body to past tense: change verbs like "Takes",
"returns", "Provides", "adds", and "exports" to past-tense forms so the
description reads as what changed. Specifically, update the sentence describing
CoreArtifactReader, the mailbox handling and returned MailboxConfig, the
deriveCoreConfig() helper, the mailboxArtifactToDerivedCoreConfig conversion in
mailbox.ts, and the exported ismArtifactToDerivedConfig from the ISM reader to
past-tense phrasing.

In `@typescript/radix-sdk/src/tests/validator-announce.e2e-test.ts`:
- Around line 37-50: RadixSigner tests currently reach into protected fields
using "as any" to grab gateway and base; add two public getter methods on
RadixSigner — getGateway() returning the existing gateway field and getBase()
returning the existing base field (similar to the existing getBaseSigner()),
then update tests to call radixSigner.getGateway() and radixSigner.getBase()
instead of casting to any.

---

Nitpick comments:
In @.changeset/mailbox-validator-announce-artifacts.md:
- Line 5: Update the changeset description to use past tense: change "establish"
to "established" and adjust any related verbs (e.g., "include" -> "included") so
the sentence reads that the new interfaces established the contract for mailbox
and validator announce artifacts, mentioning MailboxConfig,
ValidatorAnnounceConfig, ISM, and Hook artifact references in past tense.

In @.changeset/radix-validator-announce-artifacts.md:
- Line 5: The changeset message uses mixed tenses; update the description to
past tense throughout (e.g., change "provides", "fetches", "creates" to
"provided", "fetched", "created") and make the verbs consistent with the opening
past-tense verbs ("Implemented", "Combined"); specifically revise the lines
mentioning RadixValidatorAnnounceArtifactManager, validator-announce.ts, the
reader and the writer so the description uniformly describes what was changed in
past tense.

In `@typescript/deploy-sdk/src/core/core-artifact-reader.ts`:
- Around line 18-20: Move the external import ismArtifactToDerivedConfig from
`@hyperlane-xyz/provider-sdk/ism` so it appears with the other external package
imports (before the local imports HookReader/createHookReader and
IsmReader/createIsmReader); specifically, relocate the
ismArtifactToDerivedConfig import line to the top import group so external
imports are grouped together and local imports follow.

In `@typescript/radix-sdk/src/mailbox/mailbox.ts`:
- Around line 113-125: The code currently compares defaultIsmAddress to
ZERO_ADDRESS_HEX_32 using !==; change this to use the Radix address helper for
consistency and robustness by replacing the check with a call to
eqOptionalAddress (e.g., if (!eqOptionalAddress(defaultIsmAddress,
ZERO_ADDRESS_HEX_32))) before building and broadcasting the set-ISM transaction
(getSetMailboxDefaultIsmTx / this.signer.signAndBroadcast); this aligns with the
other checks in the file and prevents subtle address-comparison issues.

In `@typescript/radix-sdk/src/tests/mailbox.e2e-test.ts`:
- Around line 48-49: The test is reaching into RadixSigner internals via
(radixSigner as any).gateway and .base which breaks encapsulation; instead
either add public getters on RadixSigner (e.g., getGateway(), getBase()) and use
those in mailbox.e2e-test.ts, or change the artifact manager constructor to
accept a RadixSigner instance (e.g., new ArtifactManager(radixSigner)) and let
ArtifactManager extract gateway/base internally; if the type really is
uncertain, use unknown with a type guard that narrows to an interface exposing
gateway/base rather than using as any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4af685ec-c655-4a5b-b358-0884c72d565c

📥 Commits

Reviewing files that changed from the base of the PR and between abdbbf5 and c215b90.

📒 Files selected for processing (22)
  • .changeset/mailbox-validator-announce-artifacts.md
  • .changeset/orange-socks-return.md
  • .changeset/radix-mailbox-artifacts.md
  • .changeset/radix-validator-announce-artifacts.md
  • typescript/deploy-sdk/src/core/core-artifact-reader.ts
  • typescript/deploy-sdk/src/index.ts
  • typescript/deploy-sdk/src/ism/generic-ism.ts
  • typescript/provider-sdk/package.json
  • typescript/provider-sdk/src/mailbox.ts
  • typescript/provider-sdk/src/validator-announce.ts
  • typescript/radix-sdk/src/clients/provider.ts
  • typescript/radix-sdk/src/index.ts
  • typescript/radix-sdk/src/mailbox/mailbox-artifact-manager.ts
  • typescript/radix-sdk/src/mailbox/mailbox-query.ts
  • typescript/radix-sdk/src/mailbox/mailbox.ts
  • typescript/radix-sdk/src/tests/mailbox.e2e-test.ts
  • typescript/radix-sdk/src/tests/validator-announce.e2e-test.ts
  • typescript/radix-sdk/src/utils/base-query.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce-artifact-manager.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce-reader.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce-writer.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce.ts

Comment thread .changeset/orange-socks-return.md Outdated
Comment thread typescript/radix-sdk/src/tests/validator-announce.e2e-test.ts
Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji left a comment

Choose a reason for hiding this comment

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

Brutally Honest Review

This review was produced by running multiple parallel review agents (provider-sdk interfaces, radix-sdk implementation, deploy-sdk architecture, tests/overall) and cross-referencing with an independent Codex review. The architecture direction is sound, but the PR overclaims backward compatibility and under-tests the only part that actually matters.


Blockers

# Issue Severity
1 CoreArtifactReader creates hook reader without context.mailboxAltVMCoreReader.ts:39 and WarpReader:57 both pass { mailbox } to createHookReader(). This new reader does not. SVM hook detection (svm-sdk/src/clients/protocol.ts:56-61) requires it. This is a functional regression on Sealevel. CRITICAL
2 CoreArtifactReader.read() cannot read mailboxes with zero-address ISM/hooks — the very state this PR creates (tested in mailbox.e2e-test.ts:89). read() blindly expands zero addresses via ismReader.read(ZERO_ADDRESS) / hookReader.read(ZERO_ADDRESS), and mailboxArtifactToDerivedCoreConfig then rejects anything that isn't fully DEPLOYED. Net: the new reader is incompatible with the new writer. CRITICAL
3 Zero test coverage for CoreArtifactReader — the main architectural addition (composite reader + deriveCoreConfig() backward-compat) has no unit or e2e tests. This is exactly why issues #1 and #2 slipped through. CRITICAL

Should Fix

# Issue Severity
4 create() returns config with the requested owner, not the actual on-chain owner (the deployer). Misleads callers when config.owner !== signer. MAJOR
5 getCreateValidatorAnnounceTx duplicated verbatim in validator-announce.ts alongside existing validator-announce-tx.ts. Writer imports the new copy; existing provider.ts/signer.ts import the old. Drift bait. MAJOR
6 Inconsistent zero-address comparison in RadixMailboxWriter.create(): ISM uses raw !==, hooks use eqOptionalAddress(). Different semantics (normalization, undefined handling). MAJOR
7 Mailbox update tests (mailbox.e2e-test.ts:208, mailbox.e2e-test.ts:258) only inspect generated tx metadata, never execute the transactions or verify final on-chain state. MAJOR

Other Issues

# Issue Severity
8 mailboxArtifactToDerivedCoreConfig return type is inlined instead of referencing DerivedCoreConfig — will silently diverge if DerivedCoreConfig gains fields MINOR
9 ValidatorAnnounceConfig and RawValidatorAnnounceConfig are byte-for-byte identical — the full Raw parallel hierarchy is pure ceremony for a config with one string field MINOR
10 Precondition checks in mailboxArtifactToDerivedCoreConfig use throw new Error() instead of assert() per repo convention MINOR
11 17 commits including 4 merge commits, a typo (rmeove), and chore: make linter happy — needs squash/rebase NIT
12 Commit 7198dea has "DO NOT MERGE YET" in the title from a stacked PR — confusing in the history NIT

What's Good

  • Architecture direction is correct: composite reader delegates to specialized readers
  • ismArtifactToDerivedConfig dedup from generic-ism.ts is a clean win
  • Validator announce correctly modeled as immutable (update() returns [])
  • Radix e2e test breadth is solid: create, read, update, no-op, zero-address skip
  • Reader/writer inheritance pattern (writer extends reader) is consistent with existing ISM/hook code

Observations Outside This PR

  • AltVMCoreReader.ts:39: passes { mailbox: mailboxAddress } to createHookReader() — this is the pattern CoreArtifactReader must follow
  • WarpReader:57: same pattern — creates hook reader inside read() with mailbox context, not in constructor
  • validator-announce-tx.ts:6: existing getCreateValidatorAnnounceTx that is now duplicated

Review produced by multi-agent analysis (4 parallel review teams) + Codex cross-reference.

Comment thread typescript/deploy-sdk/src/core/core-artifact-reader.ts Outdated
Comment thread typescript/deploy-sdk/src/core/core-artifact-reader.ts
Comment thread typescript/radix-sdk/src/mailbox/mailbox.ts
Comment thread typescript/radix-sdk/src/mailbox/mailbox.ts
Comment thread typescript/radix-sdk/src/validator-announce/validator-announce.ts Outdated
Comment thread typescript/provider-sdk/src/mailbox.ts Outdated
Comment thread typescript/provider-sdk/src/mailbox.ts Outdated
Comment thread typescript/provider-sdk/src/validator-announce.ts
@xeno097 xeno097 requested review from Xaroz and yorhodes as code owners March 12, 2026 15:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
typescript/provider-sdk/src/mailbox.ts (1)

147-162: ⚠️ Potential issue | 🟡 Minor

Swap these invariant throws to assert().

These are invariant checks, not recoverable control flow. assert() keeps this helper aligned with the rest of provider-sdk.

Suggested tweak
+import { assert } from '@hyperlane-xyz/utils';
+
  // All nested artifacts should be in DEPLOYED state after CoreArtifactReader.read()
-  if (!isArtifactDeployed(defaultIsm)) {
-    throw new Error(
-      `Expected defaultIsm to be ${ArtifactState.DEPLOYED}, got ${defaultIsm.artifactState}`,
-    );
-  }
-  if (!isArtifactDeployed(defaultHook)) {
-    throw new Error(
-      `Expected defaultHook to be ${ArtifactState.DEPLOYED}, got ${defaultHook.artifactState}`,
-    );
-  }
-  if (!isArtifactDeployed(requiredHook)) {
-    throw new Error(
-      `Expected requiredHook to be ${ArtifactState.DEPLOYED}, got ${requiredHook.artifactState}`,
-    );
-  }
+  assert(
+    isArtifactDeployed(defaultIsm),
+    `Expected defaultIsm to be ${ArtifactState.DEPLOYED}, got ${defaultIsm.artifactState}`,
+  );
+  assert(
+    isArtifactDeployed(defaultHook),
+    `Expected defaultHook to be ${ArtifactState.DEPLOYED}, got ${defaultHook.artifactState}`,
+  );
+  assert(
+    isArtifactDeployed(requiredHook),
+    `Expected requiredHook to be ${ArtifactState.DEPLOYED}, got ${requiredHook.artifactState}`,
+  );
As per coding guidelines, "Use `assert()` from '@hyperlane-xyz/utils' for validating preconditions, invariants, and unexpected states rather than throwing custom errors".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/provider-sdk/src/mailbox.ts` around lines 147 - 162, Replace the
three throw Error invariant checks with assert calls from
'@hyperlane-xyz/utils': use assert(isArtifactDeployed(defaultIsm), `Expected
defaultIsm to be ${ArtifactState.DEPLOYED}, got ${defaultIsm.artifactState}`)
and similarly for defaultHook and requiredHook; update imports to include assert
if not already present and remove the corresponding throw blocks so invariants
use assert() instead of throwing custom Error.
typescript/deploy-sdk/src/core/core-artifact-reader.ts (1)

78-89: ⚠️ Potential issue | 🟠 Major

deriveCoreConfig() still breaks on empty nested refs.

Lines 80-88 now keep zero-address defaultIsm/hooks as UNDERIVED artifacts, but Lines 116-119 still feed them into mailboxArtifactToDerivedCoreConfig(), which only accepts DEPLOYED nested artifacts. So a mailbox this PR can create/read still can’t round-trip through the backward-compatible API when one of those refs is empty. Needs a regression test for that zero-address path too.

Also applies to: 113-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/deploy-sdk/src/core/core-artifact-reader.ts` around lines 78 - 89,
deriveCoreConfig() currently passes UNDERIVED nested artifacts
(rawMailbox.config.defaultIsm / defaultHook / requiredHook) into
mailboxArtifactToDerivedCoreConfig(), which expects DEPLOYED nested artifacts;
fix by, when isEmptyAddress(defaultIsmAddr|defaultHookAddr|requiredHookAddr) is
true, wrapping the rawMailbox.config.* values into a DEPLOYED-style artifact (a
minimal deployed artifact with the zero address and required type/metadata)
instead of passing the raw underived object so
mailboxArtifactToDerivedCoreConfig() receives deployed artifacts; update the
logic around defaultIsmArtifact/defaultHookArtifact/requiredHookArtifact (the
Promise.all branch using isEmptyAddress, ismReader.read, hookReader.read) and
add a regression test for the zero-address path to ensure deriveCoreConfig()
round-trips through the backward-compatible API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@typescript/radix-sdk/src/mailbox/mailbox.ts`:
- Around line 50-66: The fallback logic for mailbox addresses uses the ||
operator which treats empty strings as falsy; update the deployed.address
assignments for defaultIsm, defaultHook, and requiredHook to use the nullish
coalescing operator (??) with mailboxConfig (i.e., replace
mailboxConfig.defaultIsm || ZERO_ADDRESS_HEX_32, mailboxConfig.defaultHook ||
ZERO_ADDRESS_HEX_32, and mailboxConfig.requiredHook || ZERO_ADDRESS_HEX_32 with
mailboxConfig.defaultIsm ?? ZERO_ADDRESS_HEX_32, mailboxConfig.defaultHook ??
ZERO_ADDRESS_HEX_32, and mailboxConfig.requiredHook ?? ZERO_ADDRESS_HEX_32) so
only undefined/null fallbacks to ZERO_ADDRESS_HEX_32 (getMailboxConfig returns
string | undefined).

---

Duplicate comments:
In `@typescript/deploy-sdk/src/core/core-artifact-reader.ts`:
- Around line 78-89: deriveCoreConfig() currently passes UNDERIVED nested
artifacts (rawMailbox.config.defaultIsm / defaultHook / requiredHook) into
mailboxArtifactToDerivedCoreConfig(), which expects DEPLOYED nested artifacts;
fix by, when isEmptyAddress(defaultIsmAddr|defaultHookAddr|requiredHookAddr) is
true, wrapping the rawMailbox.config.* values into a DEPLOYED-style artifact (a
minimal deployed artifact with the zero address and required type/metadata)
instead of passing the raw underived object so
mailboxArtifactToDerivedCoreConfig() receives deployed artifacts; update the
logic around defaultIsmArtifact/defaultHookArtifact/requiredHookArtifact (the
Promise.all branch using isEmptyAddress, ismReader.read, hookReader.read) and
add a regression test for the zero-address path to ensure deriveCoreConfig()
round-trips through the backward-compatible API.

In `@typescript/provider-sdk/src/mailbox.ts`:
- Around line 147-162: Replace the three throw Error invariant checks with
assert calls from '@hyperlane-xyz/utils': use
assert(isArtifactDeployed(defaultIsm), `Expected defaultIsm to be
${ArtifactState.DEPLOYED}, got ${defaultIsm.artifactState}`) and similarly for
defaultHook and requiredHook; update imports to include assert if not already
present and remove the corresponding throw blocks so invariants use assert()
instead of throwing custom Error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4c95182-1cf7-44f2-a92f-2bccb5ee0a9d

📥 Commits

Reviewing files that changed from the base of the PR and between c215b90 and 8cb9702.

📒 Files selected for processing (11)
  • .changeset/orange-socks-return.md
  • .changeset/utils-is-empty-address.md
  • typescript/deploy-sdk/src/core/core-artifact-reader.ts
  • typescript/provider-sdk/src/mailbox.ts
  • typescript/provider-sdk/src/validator-announce.ts
  • typescript/radix-sdk/src/mailbox/mailbox.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce-artifact-manager.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce-query.ts
  • typescript/radix-sdk/src/validator-announce/validator-announce.ts
  • typescript/utils/src/addresses.ts
  • typescript/utils/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • typescript/radix-sdk/src/validator-announce/validator-announce-artifact-manager.ts
  • .changeset/orange-socks-return.md
  • typescript/provider-sdk/src/validator-announce.ts

Comment thread typescript/radix-sdk/src/mailbox/mailbox.ts
Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji left a comment

Choose a reason for hiding this comment

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

Re-Review: Post-Fix Assessment

Follow-up review after commits db8102c and 8cb9702. 6 of 12 original issues were addressed. Good progress.

Fixed

# Issue Was
1 Hook reader now created per-read with context.mailbox CRITICAL
2 read() skips expansion for empty addresses via isEmptyAddress() CRITICAL
4 create() returns this.signer.getAddress() as actual owner MAJOR
5 Validator announce writer imports from existing validator-announce-tx.js MAJOR
6 ISM check in create() now uses isEmptyAddress() consistently MAJOR
8 mailboxArtifactToDerivedCoreConfig return type explicit MINOR

Plus: new isEmptyAddress utility in @hyperlane-xyz/utils with changeset.

Still Needs Work

See inline comments. Summary:

  1. deriveCoreConfig() still breaks on zero-address mailboxesread() was fixed to return UNDERIVED artifacts, but deriveCoreConfig() passes them to mailboxArtifactToDerivedCoreConfig() which throws on non-DEPLOYED. Half-fix.
  2. Zero test coverage for CoreArtifactReader — main architectural addition, still unproven.
  3. throw vs assert() marked "fixed" but unchanged — cosmetic but misleading.

Acknowledged Won't-Fix

  • as any in tests — accepted
  • RawValidatorAnnounceConfig duplication — intentional
  • || vs ?? in reader fallbacks — intentional

Other Observations

  • Update tests ("should update mailbox when ISM changes", "should update mailbox when owner changes") only check returned tx array metadata. They never execute the transactions or verify on-chain state changed. Low priority but worth noting.
  • Import ordering in core-artifact-reader.ts:20 — external import after local imports.

Verdict

Significant improvement. The read() path is now correct. But deriveCoreConfig() — the backward-compat API this PR provides — still fails on zero-address mailboxes. That plus zero test coverage means the main addition is unproven. Not ready to approve.

Comment thread typescript/deploy-sdk/src/core/core-artifact-reader.ts
Comment thread typescript/deploy-sdk/src/core/core-artifact-reader.ts
Comment thread typescript/provider-sdk/src/mailbox.ts Outdated
@paulbalaji
Copy link
Copy Markdown
Collaborator

Follow-up note on the re-review:

Apologies — the throw vs assert() inline comment on provider-sdk/src/mailbox.ts:148 re-raises something @xeno097 already replied "fixed" to on the first review (original line 167). The code still shows throw new Error() in the latest diff, so either the fix didn't land or it was intentionally kept as-is. Either way, it's minor — please disregard if it's a conscious choice.

The deriveCoreConfig() issue on core-artifact-reader.ts:116 is separate from the read() fix xeno addressed. read() correctly returns UNDERIVED for zero addresses now (confirmed, that fix is solid). The remaining issue is that deriveCoreConfig() calls mailboxArtifactToDerivedCoreConfig() which rejects UNDERIVED — so the backward-compat API path still breaks. CodeRabbit's second incremental review also flagged this independently (lines 78-89, 113-119).

Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji left a comment

Choose a reason for hiding this comment

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

Approved

All issues from the original and re-review are now either fixed or explicitly deferred to #8147:

Fixed in this PR

  • ✅ Hook reader created per-read with context.mailbox
  • read() skips expansion for empty addresses via isEmptyAddress()
  • create() returns actual signer address as owner
  • ✅ Validator announce deduplication resolved
  • ✅ Consistent isEmptyAddress() in create()
  • DerivedCoreConfig return type explicit
  • assert() instead of throw new Error() for invariant checks (b8ca2ef)

Deferred to #8147 (acknowledged by author)

  • deriveCoreConfig() zero-address handling
  • CoreArtifactReader test coverage

The read() path — the core addition — is correct. The backward-compat deriveCoreConfig() edge case has a clear follow-up plan. Ship it.

@xeno097 xeno097 enabled auto-merge March 12, 2026 17:18
@xeno097 xeno097 added this pull request to the merge queue Mar 12, 2026
@xeno097 xeno097 removed this pull request from the merge queue due to a manual request Mar 12, 2026
…to andrey/mailbox-validator-artifact-interfaces
@xeno097 xeno097 enabled auto-merge March 12, 2026 17:29
@xeno097 xeno097 added this pull request to the merge queue Mar 12, 2026
Merged via the queue into main with commit b892e61 Mar 12, 2026
74 checks passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in Hyperlane Tasks Mar 12, 2026
@xeno097 xeno097 deleted the andrey/mailbox-validator-artifact-interfaces branch March 12, 2026 17:46
@coderabbitai coderabbitai Bot mentioned this pull request Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants