Skip to content

[FEATURE] Expose codegen'd function selectors statically, so payload builders without a wallet don't hand-write signatures #25297

Description

@shiqicao

Summary

aztec codegen already emits a selector for every method:

// generated TokenContract.d.ts
methods: {
  transfer: ((to: AztecAddressLike, amount: (bigint | number), _nonce: FieldLike)
    => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;
  ...
}

but methods is an instance member. The generated statics are only at(address, wallet),
deploy(...), artifact, artifactForPublic and storage — so reaching .selector requires
Contract.at(address, wallet), i.e. a wallet and a registered contract instance.

Anything assembling an ExecutionPayload outside a wallet-bound instance therefore cannot use the
selectors that were generated for it, and falls back to restating the ABI by hand:

FunctionCall.from({
  name: "transfer",
  to: this.#target,                                   // an AztecAddress, not a Contract
  selector: await FunctionSelector.fromSignature("transfer((Field),u128,Field)"),
  type: FunctionType.PRIVATE,
  hideMsgSender: false,
  isStatic: false,
  args: [recipient.toField(), new Fr(amount), this.#authwitNonce],
  returnTypes: [],
});

Why this is a problem

The signature string duplicates the ABI, including the encoding rules (AztecAddress(Field),
u128, …). Change a parameter type or add an argument and the string still compiles, still yields
a selector, and fails at runtime inside a private circuit with nothing pointing at the drift —
and regenerating the contract wrapper does not flag it, because nothing links the two.

The information needed to prevent this is already generated. It just isn't reachable without a
wallet.

Why the existing paths don't apply

  • contract.methods.foo.selector — needs an instance, so needs a wallet.
  • ContractFunctionInteraction.getFunctionCall() returns exactly the right object, but likewise
    needs Contract.at(address, wallet).
  • FunctionSelector.fromNameAndParameters(name, parameters) works from the artifact and needs no
    wallet, but the caller has to locate the FunctionAbi itself, which is the boilerplate the
    codegen exists to remove.

A custom FeePaymentMethod has none of what these need: getExecutionPayload() runs while the
payload is being assembled and typically holds only the callee's AztecAddress.

Proposal

Expose the already-generated selectors statically, without a wallet:

TokenContract.selectors.transfer          // FunctionSelector, no instance required

The fuller version of this — a static builder that returns a complete FunctionCall, removing the
surrounding boilerplate and the hand-encoded args too — is #25298. This issue is the smaller
piece: it removes the hand-written signature strings on their own, and is useful independently.

Who benefits

Anyone assembling an ExecutionPayload outside a wallet-bound contract instance: custom
FeePaymentMethod implementations (the sponsored-FPC pattern and anything like it), account
entrypoints, and batching/multicall helpers.

In one such implementation we carry three hand-written signature strings, and they are the only
place in that codebase where an ABI is restated by hand.

Environment

aztec 5.0.1.

Activity

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

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