Skip to content

Expose txIndexInBlock and eventIndexInTx through the wallet's getPrivateEvents API #36

Description

@SigalSwapDev

Summary

The PXE's private-event store internally tracks txIndexInBlock and eventIndexInTx and uses them to sort within a single getPrivateEvents query, but those two fields are stripped before the result reaches Wallet.getPrivateEvents callers. SDKs that merge events across multiple (contractAddress, eventSelector) queries — wallet-history features, indexers, portfolio tools — can't reconstruct the protocol-level emission ordering for events that share a (blockNumber, txHash) but came from different queries. The data exists; it's just discarded on the way out.

Current state (verified at master HEAD, 2026-05-04)

yarn-project/pxe/src/storage/private_event_store/private_event_store.ts stores both fields:

type PrivateEventMetadata = InTx & {
  contractAddress: AztecAddress;
  scope: AztecAddress;
  /** The index of the tx within the block */
  txIndexInBlock: number;
  /** The index of the event within the tx (based on nullifier position) */
  eventIndexInTx: number;
};

And uses them to sort the per-query result:

events.sort((a, b) => {
  if (a.l2BlockNumber !== b.l2BlockNumber) return a.l2BlockNumber - b.l2BlockNumber;
  if (a.txIndexInBlock !== b.txIndexInBlock) return a.txIndexInBlock - b.txIndexInBlock;
  return a.eventIndexInTx - b.eventIndexInTx;
});

But PackedPrivateEvent (yarn-project/pxe/src/pxe.ts) drops them:

export type PackedPrivateEvent = InTx & {
  packedEvent: Fr[];
  eventSelector: EventSelector;
};

And Wallet.getPrivateEvents (yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts) trims further:

return {
  event: decodeFromAbi([eventDef.abiType], pxeEvent.packedEvent) as T,
  metadata: {
    l2BlockNumber: pxeEvent.l2BlockNumber,
    l2BlockHash: pxeEvent.l2BlockHash,
    txHash: pxeEvent.txHash,
  },
};

Why this is a problem

A privacy-preserving DEX's wallet-history feature renders the user's swap and liquidity history merged across:

  • pair-direct private events (one query per (pair, eventSelector))
  • router-mediated private events (one query per (router, eventSelector))

For users who interact with the SDK directly, each tx emits at most one event per (contractAddress, eventSelector) bucket, so a (blockNumber, txHash) sort is sufficient. But for users who integrate via a batch / wrapper contract (a common pattern for gas-aggregating multi-hop or multi-trade UX), one atomic tx can emit several events of different selectors from one contract, OR the same selector from multiple contracts. Without eventIndexInTx, those events can only be ordered lexically by selector, which doesn't match emission order. Cost-basis tools (FIFO/LIFO accounting), running-balance reconstructions, and any "chronological feed" UX get non-deterministic answers.

Proposed change

Add the two indices to PackedPrivateEvent:

export type PackedPrivateEvent = InTx & {
  packedEvent: Fr[];
  eventSelector: EventSelector;
  txIndexInBlock: number;
  eventIndexInTx: number;
};

And surface them through Wallet.getPrivateEvents:

metadata: {
  l2BlockNumber: pxeEvent.l2BlockNumber,
  l2BlockHash: pxeEvent.l2BlockHash,
  txHash: pxeEvent.txHash,
  txIndexInBlock: pxeEvent.txIndexInBlock,
  eventIndexInTx: pxeEvent.eventIndexInTx,
},

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions