Skip to content

[BUG] SimulateInteractionOptions re-declares an identical fee; the Omit<..., 'fee'> implies a distinction from SendInteractionOptions that does not exist #25302

Description

@shiqicao

What are you trying to do?

Write a helper in application code that builds fee options once and passes them to both .simulate() and .send(), and understand from the type declarations whether the two accept the same fee.

The declarations say they differ, but they don't. In yarn-project/aztec.js/src/contract/interaction_options.ts, SimulateInteractionOptions omits fee from SendInteractionOptions and then re-declares it with the identical type:

// line 99
export type SendInteractionOptionsWithoutWait = RequestInteractionOptions & {
  from: AztecAddress | NoFrom;
  /** The fee options for the transaction. */
  fee?: InteractionFeeOptions;
  ...
};

// line 138
export type SimulateInteractionOptions = Omit<SendInteractionOptions, 'fee'> & {
  /** The fee options for the transaction. */
  fee?: InteractionFeeOptions;
  skipTxValidation?: boolean;
  ...
};

The Omit<..., 'fee'> + re-add is a no-op. toSendOptions and toSimulateOptions also treat the field identically (spread paymentMethod.getGasSettings(), override with fee.gasSettings, forward congestionEstimate).

To be upfront: this produces no incorrect runtime behavior that I can find — it's a defect in the types, not in execution. The cost is that the Omit actively signals a distinction between simulate and send fee handling that does not exist, so a reader has to diff the two declarations to establish that they're the same, and a future edit to SendInteractionOptions.fee will silently not reach simulate. My guess is it's a leftover from when send's fee was the narrower payment-method-only type.

Code Reference

The redundancy is demonstrable at the type level — both assertions compile, i.e. the two option types are mutually assignable in the fee field:

import type { SendInteractionOptions, SimulateInteractionOptions } from '@aztec/aztec.js';

// Both compile: the Omit + re-add changes nothing.
type A = SimulateInteractionOptions['fee'];
type B = SendInteractionOptions['fee'];
const _a: A = null as unknown as B;
const _b: B = null as unknown as A;

// So this helper is silently valid for both, which the declarations obscure:
const fee = { paymentMethod, gasSettings: { gasLimits } };
await contract.methods.foo().simulate({ from, fee });
await contract.methods.foo().send({ from, fee });

Two adjacent type defects fall out of the same area:

  1. Because the Omit drops only fee and not wait, SimulateInteractionOptions inherits wait?: undefined from SendInteractionOptions<undefined>. So simulate({ from, wait: undefined }) type-checks even though wait is meaningless for a simulation, and simulate({ from, wait: NO_WAIT }) fails with a mismatch against undefined rather than an "unknown property" error. Basing simulate on SendInteractionOptionsWithoutWait would express the intent.

  2. RequestInteractionOptions.fee (line 59) is FeePaymentMethodOption — genuinely narrower than InteractionFeeOptions. But SendInteractionOptionsWithoutWait intersects with it and re-declares fee?: InteractionFeeOptions, and since InteractionFeeOptions = GasSettingsOption & FeePaymentMethodOption, the intersection collapses back to InteractionFeeOptions. The narrower declaration is therefore invisible on send and simulate and only constrains consumers using RequestInteractionOptions directly. If request-level options really are meant to be payment-method-only, the send-side shadowing hides that.

Suggested fix for the main point, type-identical to today:

export type SimulateInteractionOptions = SendInteractionOptions & {
  skipTxValidation?: boolean;
  skipFeeEnforcement?: boolean;
  includeMetadata?: boolean;
  overrides?: SimulationOverrides;
};

Aztec Version

5.0.1 (commit 72666f8)

OS

Linux

Node Version

v24.18.0

Additional Context

Found while reading the aztec.js sources rather than from a failing test. Happy to open a PR if the simplification is wanted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions