Skip to main content

Architecture

This page talks about the architecture of a wallet in Aztec.

To get an overview about wallets in Aztec, go here.

To learn how to write an accounts contract, go here.

To create a schnorr account in the sandbox, go here.

Wallets expose to dapps an interface that allows them to act on behalf of the user, such as querying private state or sending transactions. Bear mind that, as in Ethereum, wallets should require user confirmation whenever carrying out a potentially sensitive action requested by a dapp.

Overview

Architecture-wise, a wallet is an instance of an Private Execution Environment (PXE) which manages user keys and private state. The PXE also communicates with an Aztec Node for retrieving public information or broadcasting transactions. Note that the PXE requires a local database for keeping private state, and is also expected to be continuously syncing new blocks for trial-decryption of user notes.

Additionally, a wallet must be able to handle one or more account contract implementations. When a user creates a new account, the account is represented on-chain by an account contract. The wallet is responsible for deploying and interacting with this contract. A wallet may support multiple flavours of accounts, such as an account that uses ECDSA signatures, or one that relies on WebAuthn, or one that requires multi-factor authentication. For a user, the choice of what account implementation to use is then determined by the wallet they interact with.

In code, this translates to a wallet implementing an AccountInterface interface that defines how to create an execution request out of an array of function calls for the specific implementation of an account contract and how to generate an auth witness for authorizing actions on behalf of the user. Think of this interface as the Javascript counterpart of an account contract, or the piece of code that knows how to format a transaction and authenticate an action based on the rules defined by the user's account contract implementation.

Account interface

The account interface is used for creating an execution request out of one or more function calls requested by a dapp, as well as creating an auth witness for a given message hash. Account contracts are expected to handle multiple function calls per transaction, since dapps may choose to batch multiple actions into a single request to the wallet.

account-interface
/** Creates authorization witnesses. */
export interface AuthWitnessProvider {
/**
* Create an authorization witness for the given message.
* @param message - Message to authorize.
*/
createAuthWitness(message: Fr): Promise<AuthWitness>;
}

/** Creates transaction execution requests out of a set of function calls. */
export interface EntrypointInterface {
/**
* Generates an authenticated request out of set of function calls.
* @param executions - The execution intents to be run.
* @param feeOpts - The fee to be paid for the transaction.
* @returns The authenticated transaction execution request.
*/
createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise<TxExecutionRequest>;
}

/**
* Handler for interfacing with an account. Knows how to create transaction execution
* requests and authorize actions for its corresponding account.
*/
export interface AccountInterface extends AuthWitnessProvider, EntrypointInterface {
/**
* Returns the complete address for this account.
*/
getCompleteAddress(): CompleteAddress;
}
Source code: /yarn-project/aztec.js/src/account/interface.ts#L16-L47 (aztec-packages-v0.24.0)

Refer to the page on writing an account contract for an example on how to implement this interface.

PXE interface

A wallet exposes the PXE interface to dapps by running an PXE instance. The PXE requires a keystore and a database implementation for storing keys, private state, and recipient encryption public keys.

pxe-interface
/**
* Private eXecution Environment (PXE) runs locally for each user, providing functionality for all the operations
* needed to interact with the Aztec network, including account management, private data management,
* transaction local simulation, and access to an Aztec node. This interface, as part of a Wallet,
* is exposed to dapps for interacting with the network on behalf of the user.
*/
export interface PXE {
/**
* Insert an auth witness for a given message hash. Auth witnesses are used to authorize actions on
* behalf of a user. For instance, a token transfer initiated by a different address may request
* authorization from the user to move their tokens. This authorization is granted by the user
* account contract by verifying an auth witness requested to the execution oracle. Witnesses are
* usually a signature over a hash of the action to be authorized, but their actual contents depend
* on the account contract that consumes them.
*
* @param authWitness - The auth witness to insert. Composed of an identifier, which is the hash of
* the action to be authorized, and the actual witness as an array of fields, which are to be
* deserialized and processed by the account contract.
*/
addAuthWitness(authWitness: AuthWitness): Promise<void>;

/**
* Adding a capsule to the capsule dispenser.
* @param capsule - An array of field elements representing the capsule.
* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.
*/
addCapsule(capsule: Fr[]): Promise<void>;

/**
* Registers a user account in PXE given its master encryption private key.
* Once a new account is registered, the PXE Service will trial-decrypt all published notes on
* the chain and store those that correspond to the registered account. Will do nothing if the
* account is already registered.
*
* @param privKey - Private key of the corresponding user master public key.
* @param partialAddress - The partial address of the account contract corresponding to the account being registered.
* @returns The complete address of the account.
*/
registerAccount(privKey: GrumpkinPrivateKey, partialAddress: PartialAddress): Promise<CompleteAddress>;

/**
* Registers a recipient in PXE. This is required when sending encrypted notes to
* a user who hasn't deployed their account contract yet. Since their account is not deployed, their
* encryption public key has not been broadcasted, so we need to manually register it on the PXE Service
* in order to be able to encrypt data for this recipient.
*
* @param recipient - The complete address of the recipient
* @remarks Called recipient because we can only send notes to this account and not receive them via this PXE Service.
* This is because we don't have the associated private key and for this reason we can't decrypt
* the recipient's notes. We can send notes to this account because we can encrypt them with the recipient's
* public key.
*/
registerRecipient(recipient: CompleteAddress): Promise<void>;

/**
* Retrieves the user accounts registered on this PXE Service.
* @returns An array of the accounts registered on this PXE Service.
*/
getRegisteredAccounts(): Promise<CompleteAddress[]>;

/**
* Retrieves the complete address of the account corresponding to the provided aztec address.
* Complete addresses include the address, the partial address, and the encryption public key.
*
* @param address - The address of account.
* @returns The complete address of the requested account if found.
*/
getRegisteredAccount(address: AztecAddress): Promise<CompleteAddress | undefined>;

/**
* Retrieves the recipients added to this PXE Service.
* @returns An array of recipients registered on this PXE Service.
*/
getRecipients(): Promise<CompleteAddress[]>;

/**
* Retrieves the complete address of the recipient corresponding to the provided aztec address.
* Complete addresses include the address, the partial address, and the encryption public key.
*
* @param address - The aztec address of the recipient.
* @returns The complete address of the requested recipient.
*/
getRecipient(address: AztecAddress): Promise<CompleteAddress | undefined>;

/**
* Adds deployed contracts to the PXE Service. Deployed contract information is used to access the
* contract code when simulating local transactions. This is automatically called by aztec.js when
* deploying a contract. Dapps that wish to interact with contracts already deployed should register
* these contracts in their users' PXE Service through this method.
*
* @param contracts - An array of DeployedContract objects containing contract ABI, address, and portal contract.
*/
addContracts(contracts: DeployedContract[]): Promise<void>;

/**
* Retrieves the addresses of contracts added to this PXE Service.
* @returns An array of contracts addresses registered on this PXE Service.
*/
getContracts(): Promise<AztecAddress[]>;

/**
* Creates a transaction based on the provided preauthenticated execution request. This will
* run a local simulation of the private execution (and optionally of public as well), assemble
* the zero-knowledge proof for the private execution, and return the transaction object.
*
* @param txRequest - An authenticated tx request ready for simulation
* @param simulatePublic - Whether to simulate the public part of the transaction.
* @returns A transaction ready to be sent to the network for execution.
* @throws If the code for the functions executed in this transaction has not been made available via `addContracts`.
*/
simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean): Promise<Tx>;

/**
* Sends a transaction to an Aztec node to be broadcasted to the network and mined.
* @param tx - The transaction as created via `simulateTx`.
* @returns A hash of the transaction, used to identify it.
*/
sendTx(tx: Tx): Promise<TxHash>;

/**
* Fetches a transaction receipt for a given transaction hash. Returns a mined receipt if it was added
* to the chain, a pending receipt if it's still in the mempool of the connected Aztec node, or a dropped
* receipt if not found in the connected Aztec node.
*
* @param txHash - The transaction hash.
* @returns A receipt of the transaction.
*/
getTxReceipt(txHash: TxHash): Promise<TxReceipt>;

/**
* Fetches a transaction by its hash.
* @param txHash - The transaction hash
* @returns A transaction object or undefined if the transaction hasn't been mined yet
*/
getTx(txHash: TxHash): Promise<L2Tx | undefined>;

/**
* Gets the storage value at the given contract storage slot.
*
* @remarks The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree.
* Aztec's version of `eth_getStorageAt`.
*
* @param contract - Address of the contract to query.
* @param slot - Slot to query.
* @returns Storage value at the given contract slot.
* @throws If the contract is not deployed.
*/
getPublicStorageAt(contract: AztecAddress, slot: Fr): Promise<Fr>;

/**
* Gets notes of accounts registered in this PXE based on the provided filter.
* @param filter - The filter to apply to the notes.
* @returns The requested notes.
*/
getNotes(filter: NoteFilter): Promise<ExtendedNote[]>;

/**
* Adds a note to the database.
* @throws If the note hash of the note doesn't exist in the tree.
* @param note - The note to add.
*/
addNote(note: ExtendedNote): Promise<void>;

/**
* Get the given block.
* @param number - The block number being requested.
* @returns The blocks requested.
*/
getBlock(number: number): Promise<L2Block | undefined>;

/**
* Simulate the execution of a view (read-only) function on a deployed contract without actually modifying state.
* This is useful to inspect contract state, for example fetching a variable value or calling a getter function.
* The function takes function name and arguments as parameters, along with the contract address
* and optionally the sender's address.
*
* @param functionName - The name of the function to be called in the contract.
* @param args - The arguments to be provided to the function.
* @param to - The address of the contract to be called.
* @param from - (Optional) The msg sender to set for the call.
* @returns The result of the view function call, structured based on the function ABI.
*/
viewTx(functionName: string, args: any[], to: AztecAddress, from?: AztecAddress): Promise<any>;

/**
* Gets the extended contract data for this contract. Extended contract data includes the address,
* portal contract address on L1, public functions, partial address, and encryption public key.
*
* @param contractAddress - The contract's address.
* @returns The extended contract data if found.
*/
getExtendedContractData(contractAddress: AztecAddress): Promise<ExtendedContractData | undefined>;

/**
* Gets the portal contract address on L1 for the given contract.
*
* @param contractAddress - The contract's address.
* @returns The contract's portal address if found.
*/
getContractData(contractAddress: AztecAddress): Promise<ContractData | undefined>;

/**
* Gets unencrypted logs based on the provided filter.
* @param filter - The filter to apply to the logs.
* @returns The requested logs.
*/
getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse>;

/**
* Fetches the current block number.
* @returns The block number.
*/
getBlockNumber(): Promise<number>;

/**
* Returns the information about the server's node. Includes current Node version, compatible Noir version,
* L1 chain identifier, protocol version, and L1 address of the rollup contract.
* @returns - The node information.
*/
getNodeInfo(): Promise<NodeInfo>;

/**
* Checks whether all the blocks were processed (tree roots updated, txs updated with block info, etc.).
* @returns True if there are no outstanding blocks to be synched.
* @remarks This indicates that blocks and transactions are synched even if notes are not. Compares local block number with the block number from aztec node.
* @deprecated Use `getSyncStatus` instead.
*/
isGlobalStateSynchronized(): Promise<boolean>;

/**
* Checks if the specified account is synchronized.
* @param account - The aztec address for which to query the sync status.
* @returns True if the account is fully synched, false otherwise.
* @deprecated Use `getSyncStatus` instead.
* @remarks Checks whether all the notes from all the blocks have been processed. If it is not the case, the
* retrieved information from contracts might be old/stale (e.g. old token balance).
* @throws If checking a sync status of account which is not registered.
*/
isAccountStateSynchronized(account: AztecAddress): Promise<boolean>;

/**
* Returns the latest block that has been synchronized globally and for each account. The global block number
* indicates whether global state has been updated up to that block, whereas each address indicates up to which
* block the private state has been synced for that account.
* @returns The latest block synchronized for blocks, and the latest block synched for notes for each public key being tracked.
*/
getSyncStatus(): Promise<SyncStatus>;

/**
* Returns a Contact Instance given its address, which includes the contract class identifier, portal address,
* initialization hash, deployment salt, and public keys hash.
* TOOD(@spalladino): Should we return the public keys in plain as well here?
* @param address
*/
getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined>;
}
Source code: /yarn-project/circuit-types/src/interfaces/pxe.ts#L17-L274 (aztec-packages-v0.24.0)