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:
-
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.
-
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.
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 samefee.The declarations say they differ, but they don't. In
yarn-project/aztec.js/src/contract/interaction_options.ts,SimulateInteractionOptionsomitsfeefromSendInteractionOptionsand then re-declares it with the identical type:The
Omit<..., 'fee'>+ re-add is a no-op.toSendOptionsandtoSimulateOptionsalso treat the field identically (spreadpaymentMethod.getGasSettings(), override withfee.gasSettings, forwardcongestionEstimate).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
Omitactively 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 toSendInteractionOptions.feewill silently not reach simulate. My guess is it's a leftover from when send'sfeewas 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
feefield:Two adjacent type defects fall out of the same area:
Because the
Omitdrops onlyfeeand notwait,SimulateInteractionOptionsinheritswait?: undefinedfromSendInteractionOptions<undefined>. Sosimulate({ from, wait: undefined })type-checks even thoughwaitis meaningless for a simulation, andsimulate({ from, wait: NO_WAIT })fails with a mismatch againstundefinedrather than an "unknown property" error. Basing simulate onSendInteractionOptionsWithoutWaitwould express the intent.RequestInteractionOptions.fee(line 59) isFeePaymentMethodOption— genuinely narrower thanInteractionFeeOptions. ButSendInteractionOptionsWithoutWaitintersects with it and re-declaresfee?: InteractionFeeOptions, and sinceInteractionFeeOptions = GasSettingsOption & FeePaymentMethodOption, the intersection collapses back toInteractionFeeOptions. The narrower declaration is therefore invisible on send and simulate and only constrains consumers usingRequestInteractionOptionsdirectly. 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:
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.