Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Confirm the account was deployed successfully:

```typescript title="verify_account_deployment" showLineNumbers
const metadata = await wallet.getContractMetadata(feeJuiceAccount.address);
console.log("Account deployed:", metadata.isContractInitialized);
console.log("Account deployed:", metadata.initializationStatus);
```
> <sup><sub><a href="https://github.com/AztecProtocol/aztec-packages/blob/v5.0.0-nightly.20260319/docs/examples/ts/aztecjs_connection/index.ts#L157-L160" target="_blank" rel="noopener noreferrer">Source code: docs/examples/ts/aztecjs_connection/index.ts#L157-L160</a></sub></sup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ const metadata = await wallet.getContractMetadata(contractAddress);
metadata.instance; // Contract registered in your wallet?
metadata.isContractClassPubliclyRegistered; // Class registered on the network?
metadata.isContractPublished; // Instance registered on the network?
metadata.isContractInitialized; // Constructor has been called?
metadata.initializationStatus; // Constructor has been called?
```

For a complete overview of what these states mean and when functions become callable, see [Contract Readiness States](../aztec-nr/contract_readiness_states.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const metadata = await wallet.getContractMetadata(contractAddress);
metadata.instance; // Contract registered in your wallet?
metadata.isContractClassPubliclyRegistered; // Class registered on the network?
metadata.isContractPublished; // Instance registered on the network?
metadata.isContractInitialized; // Constructor has been called?
metadata.initializationStatus; // Constructor has been called?
```

For a complete overview of what these states mean and when functions become callable, see [Contract Readiness States](../aztec-nr/contract_readiness_states.md).
Expand Down
23 changes: 22 additions & 1 deletion docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum

`ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`.

- `INITIALIZED`: the contract has been initialized (initialization nullifier found)
- `UNINITIALIZED`: the contract instance is registered but has not been initialized
- `UNKNOWN`: the instance is not registered and no public initialization nullifier was found

When the instance is not registered, the wallet now attempts to check the public initialization nullifier (computed from address alone) before returning `UNKNOWN`. Previously this case returned `undefined`.

**Migration:**

```diff
+ import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';

const metadata = await wallet.getContractMetadata(address);
- if (metadata.isContractInitialized) {
+ if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) {
// contract is initialized
}
```

### [Aztec.js] Use `NO_FROM` instead of `AztecAddress.ZERO` to bypass account contract entrypoint

When sending transactions that should not be mediated by an account contract (e.g., account contract self-deployments), use the explicit `NO_FROM` sentinel instead of `AztecAddress.ZERO`.
Expand Down Expand Up @@ -64,7 +86,6 @@ The `scope` field in `ExecuteUtilityOptions` has been renamed to `scopes` and ch
```

**Impact**: Any code that calls `wallet.executeUtility` directly must update the options object. Wallets must update to adapt to the new interface

### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one

The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`).
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/ts/aztecjs_connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ await deployMethodFeeJuice.send({

// docs:start:verify_account_deployment
const metadata = await wallet.getContractMetadata(feeJuiceAccount.address);
console.log("Account deployed:", metadata.isContractInitialized);
console.log("Account deployed:", metadata.initializationStatus);
// docs:end:verify_account_deployment
6 changes: 3 additions & 3 deletions playground/src/components/navbar/components/WalletHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from '@aztec/wallet-sdk/manager';
import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
import { Fr } from '@aztec/foundation/curves/bn254';
import type { Wallet } from '@aztec/aztec.js/wallet';
import { ContractInitializationStatus, type Wallet } from '@aztec/aztec.js/wallet';

type ExtendedWalletProvider = Omit<WalletProvider, 'type'> & {
type: WalletProvider['type'] | 'embedded';
Expand Down Expand Up @@ -86,8 +86,8 @@ async function discoverTestAccounts(wallet: EmbeddedWallet) {
return;
}

const { isContractInitialized } = await wallet.getContractMetadata(sampleAccount.address);
if (!isContractInitialized) {
const { initializationStatus } = await wallet.getContractMetadata(sampleAccount.address);
if (initializationStatus !== ContractInitializationStatus.INITIALIZED) {
return;
}

Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/api/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
type PublicEvent,
type PublicEventFilter,
type ContractMetadata,
ContractInitializationStatus,
type ContractClassMetadata,
AppCapabilitiesSchema,
WalletCapabilitiesSchema,
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/aztec.js/src/wallet/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type {
SimulateOptions,
Wallet,
} from './wallet.js';
import { WalletSchema } from './wallet.js';
import { ContractInitializationStatus, WalletSchema } from './wallet.js';

describe('WalletSchema', () => {
let handler: MockWallet;
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('WalletSchema', () => {
const result = await context.client.getContractMetadata(await AztecAddress.random());
expect(result).toEqual({
instance: undefined,
isContractInitialized: undefined,
initializationStatus: ContractInitializationStatus.UNKNOWN,
isContractPublished: expect.any(Boolean),
isContractUpdated: expect.any(Boolean),
updatedContractClassId: undefined,
Expand Down Expand Up @@ -408,7 +408,7 @@ class MockWallet implements Wallet {
getContractMetadata(_address: AztecAddress): Promise<ContractMetadata> {
return Promise.resolve({
instance: undefined,
isContractInitialized: undefined,
initializationStatus: ContractInitializationStatus.UNKNOWN,
isContractPublished: false,
isContractUpdated: false,
updatedContractClassId: undefined,
Expand Down
22 changes: 16 additions & 6 deletions yarn-project/aztec.js/src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,27 @@ export type PublicEvent<T> = Event<
}
>;

/** Whether the contract has been initialized. */
export enum ContractInitializationStatus {
/** The contract has been initialized (initialization nullifier found). */
INITIALIZED = 'INITIALIZED',
/** The contract has not been initialized (instance is known, but no initialization nullifier found). */
UNINITIALIZED = 'UNINITIALIZED',
/**
* Initialization status cannot be determined. The contract instance is not registered in this wallet, so we have
* limited ability to check for initialization. The contract may or may not have been initialized.
*/
UNKNOWN = 'UNKNOWN',
}

/**
* Contract metadata including deployment and registration status.
*/
export type ContractMetadata = {
/** The contract instance */
instance?: ContractInstanceWithAddress;
/**
* Whether the contract has been initialized (initialization nullifier exists).
* Undefined when instance is not registered.
*/
isContractInitialized: boolean | undefined;
/** Whether the contract has been initialized. */
initializationStatus: ContractInitializationStatus;
/** Whether the contract instance is publicly deployed on-chain */
isContractPublished: boolean;
/** Whether the contract has been updated to a different class */
Expand Down Expand Up @@ -377,7 +387,7 @@ export const PublicEventSchema = zodFor<PublicEvent<AbiDecoded>>()(

export const ContractMetadataSchema = z.object({
instance: optional(ContractInstanceWithAddressSchema),
isContractInitialized: optional(z.boolean()),
initializationStatus: z.nativeEnum(ContractInitializationStatus),
isContractPublished: z.boolean(),
isContractUpdated: z.boolean(),
updatedContractClassId: optional(schemas.Fr),
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/bot/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { deriveKeys } from '@aztec/aztec.js/keys';
import { createLogger } from '@aztec/aztec.js/log';
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
import { waitForTx } from '@aztec/aztec.js/node';
import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { createEthereumChain } from '@aztec/ethereum/chain';
import { createExtendedL1Client } from '@aztec/ethereum/client';
import { RollupContract } from '@aztec/ethereum/contracts';
Expand Down Expand Up @@ -208,7 +209,7 @@ export class BotFactory {
const signingKey = deriveSigningKey(secret);
const accountManager = await this.wallet.createSchnorrAccount(secret, salt, signingKey);
const metadata = await this.wallet.getContractMetadata(accountManager.address);
if (metadata.isContractInitialized) {
if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) {
this.log.info(`Account at ${accountManager.address.toString()} already initialized`);
const timer = new Timer();
const address = accountManager.address;
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/end-to-end/src/e2e_block_building.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import { type AztecNode, waitForTx } from '@aztec/aztec.js/node';
import { TxStatus } from '@aztec/aztec.js/tx';
import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { AnvilTestWatcher, CheatCodes } from '@aztec/aztec/testing';
import { asyncMap } from '@aztec/foundation/async-map';
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
Expand Down Expand Up @@ -176,9 +177,9 @@ describe('e2e_block_building', () => {

// Assert all contracts got initialized
const areInitialized = await Promise.all(
addresses.map(async a => (await wallet.getContractMetadata(a)).isContractInitialized),
addresses.map(async a => (await wallet.getContractMetadata(a)).initializationStatus),
);
expect(areInitialized).toEqual(times(TX_COUNT, () => true));
expect(areInitialized).toEqual(times(TX_COUNT, () => ContractInitializationStatus.INITIALIZED));
});

it('assembles a block with multiple txs with public fns', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BatchCall } from '@aztec/aztec.js/contracts';
import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node';
import type { Wallet } from '@aztec/aztec.js/wallet';
import { ContractInitializationStatus, type Wallet } from '@aztec/aztec.js/wallet';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import { CounterContract } from '@aztec/noir-test-contracts.js/Counter';
import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest';
Expand Down Expand Up @@ -201,8 +201,23 @@ describe('e2e_deploy_contract deploy method', () => {
publicCallTxPromise,
]);
expect(deployTxReceipt.blockNumber).toEqual(publicCallTxReceipt.blockNumber);

await t.aztecNodeAdmin.setConfig({ minTxsPerBlock: 1 });
}, 300_000);

it('reports YES for initialization status via public nullifier when instance is not registered', async () => {
const owner = defaultAccountAddress;
const { contract } = await StatefulTestContract.deploy(wallet, owner, 42).send({ from: defaultAccountAddress });

// StatefulTestContract has public functions with initialization checks, so during deployment and initialization
// it emits a public initialization nullifier. A wallet without the instance registered falls back to checking
// this nullifier.
const secondWallet = await TestWallet.create(aztecNode);
const metadata = await secondWallet.getContractMetadata(contract.address);
expect(metadata.instance).toBeUndefined();
expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.INITIALIZED);
});

describe('regressions', () => {
it('fails properly when trying to deploy a contract with a failing constructor with a pxe client with retries', async () => {
const { AZTEC_NODE_URL } = process.env;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { publishContractClass, publishInstance } from '@aztec/aztec.js/deploymen
import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import type { AztecNode } from '@aztec/aztec.js/node';
import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest';
import { NoConstructorContract } from '@aztec/noir-test-contracts.js/NoConstructor';
import { PrivateInitTestContract } from '@aztec/noir-test-contracts.js/PrivateInitTest';
Expand Down Expand Up @@ -224,6 +225,32 @@ describe('e2e_deploy_contract private initialization', () => {
);
});

describe('initialization status', () => {
it('reports INITIALIZED when contract is registered and initialized', async () => {
const contract = await t.registerContract(wallet, PrivateInitTestContract, {
initArgs: [42],
constructorName: 'initialize',
});
await contract.methods.initialize(42).send({ from: defaultAccountAddress });
const metadata = await wallet.getContractMetadata(contract.address);
expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.INITIALIZED);
});

it('reports UNINITIALIZED when contract is registered but not initialized', async () => {
const contract = await t.registerContract(wallet, PrivateInitTestContract, {
initArgs: [42],
constructorName: 'initialize',
});
const metadata = await wallet.getContractMetadata(contract.address);
expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.UNINITIALIZED);
});

it('reports UNKNOWN when contract instance is not registered', async () => {
const metadata = await wallet.getContractMetadata(await AztecAddress.random());
expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.UNKNOWN);
});
});

/** Registers a contract instance locally and publishes it on-chain (so sequencers can find public function's bytecode). */
async function registerAndPublishContract(
initArgs: InitTestCtorArgs,
Expand Down
61 changes: 34 additions & 27 deletions yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ import {
} from '@aztec/aztec.js/contracts';
import type { FeePaymentMethod } from '@aztec/aztec.js/fee';
import { waitForTx } from '@aztec/aztec.js/node';
import type {
Aliased,
AppCapabilities,
BatchResults,
BatchedMethod,
ExecuteUtilityOptions,
PrivateEvent,
PrivateEventFilter,
ProfileOptions,
SendOptions,
SimulateOptions,
Wallet,
WalletCapabilities,
import {
type Aliased,
type AppCapabilities,
type BatchResults,
type BatchedMethod,
ContractInitializationStatus,
type ExecuteUtilityOptions,
type PrivateEvent,
type PrivateEventFilter,
type ProfileOptions,
type SendOptions,
type SimulateOptions,
type Wallet,
type WalletCapabilities,
} from '@aztec/aztec.js/wallet';
import {
GAS_ESTIMATION_DA_GAS_LIMIT,
Expand Down Expand Up @@ -52,7 +53,10 @@ import {
} from '@aztec/stdlib/contract';
import { SimulationError } from '@aztec/stdlib/errors';
import { Gas, GasSettings } from '@aztec/stdlib/gas';
import { computeSiloedPrivateInitializationNullifier } from '@aztec/stdlib/hash';
import {
computeSiloedPrivateInitializationNullifier,
computeSiloedPublicInitializationNullifier,
} from '@aztec/stdlib/hash';
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
import {
BlockHeader,
Expand Down Expand Up @@ -494,34 +498,37 @@ export abstract class BaseWallet implements Wallet {

/**
* Returns metadata about a contract, including whether it has been initialized, published, and updated.
*
* `isContractInitialized` requires the contract instance to be registered in the PXE (for `init_hash`). When the
* instance is not available, `isContractInitialized` is `undefined` since it cannot be determined.
* @param address - The contract address to query.
*/
async getContractMetadata(address: AztecAddress) {
const instance = await this.pxe.getContractInstance(address);
const publiclyRegisteredContractPromise = this.aztecNode.getContract(address);
// We check only the private initialization nullifier. It is emitted by both private and public initializers and
// includes init_hash, preventing observers from determining initialization status from the address alone. Without
// the instance (and thus init_hash), we can't compute it, so we return undefined.
//
// We skip the public initialization nullifier because it's not always emitted (contracts without public external
// functions that require initialization checks won't emit it). If the private one exists, the public one was
// created in the same tx and will also be present.
let isContractInitialized: boolean | undefined = undefined;

let initializationStatus: ContractInitializationStatus;
if (instance) {
// We have the instance, so we can compute the private initialization nullifier (which includes init_hash and is
// emitted by both private and public initializers) and get a definitive INITIALIZED/UNINITIALIZED answer.
const initNullifier = await computeSiloedPrivateInitializationNullifier(address, instance.initializationHash);
const witness = await this.aztecNode.getNullifierMembershipWitness('latest', initNullifier);
isContractInitialized = !!witness;
initializationStatus = witness
? ContractInitializationStatus.INITIALIZED
: ContractInitializationStatus.UNINITIALIZED;
} else {
// Without the instance we lack the init_hash needed for the private nullifier. We fall back to checking the
// public initialization nullifier (computed from address alone). Not all contracts emit it (only those with
// public functions that require initialization checks), so its absence doesn't mean the contract is
// uninitialized.
const publicNullifier = await computeSiloedPublicInitializationNullifier(address);
const witness = await this.aztecNode.getNullifierMembershipWitness('latest', publicNullifier);
initializationStatus = witness ? ContractInitializationStatus.INITIALIZED : ContractInitializationStatus.UNKNOWN;
}
const publiclyRegisteredContract = await publiclyRegisteredContractPromise;
const isContractUpdated =
publiclyRegisteredContract &&
!publiclyRegisteredContract.currentContractClassId.equals(publiclyRegisteredContract.originalContractClassId);
return {
instance: instance ?? undefined,
isContractInitialized,
initializationStatus,
isContractPublished: !!publiclyRegisteredContract,
isContractUpdated: !!isContractUpdated,
updatedContractClassId: isContractUpdated ? publiclyRegisteredContract.currentContractClassId : undefined,
Expand Down
Loading