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
10 changes: 4 additions & 6 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ jobs:
- name: Update path
run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH

- name: Set Aztec version and start sandbox
- name: Set Aztec version and start local network
run: |
aztec-up 3.0.0-devnet.4
aztec start --sandbox &
aztec-up 3.0.0-devnet.20251212
aztec start --local-network &

- name: Install dependencies
working-directory: ./app
run: npm install -g yarn && yarn
run: yarn install

- name: Install Playwright Browsers
working-directory: ./app
run: yarn playwright install --with-deps

- name: Build
Expand Down
59 changes: 29 additions & 30 deletions APPLICATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ A **nullifier** is a unique cryptographic value that:
```typescript
// From contracts/src/main.nr:48
let nullifier = poseidon2_hash([
context.msg_sender().unwrap().to_field(),
self.msg_sender().unwrap().to_field(),
secret,
]);
context.push_nullifier(nullifier);
self.context.push_nullifier(nullifier);
```

### 3. Sponsored Fee Payment
Expand Down Expand Up @@ -158,9 +158,9 @@ struct Storage<Context> {

```noir
fn constructor(admin: AztecAddress) {
storage.admin.write(admin);
storage.vote_ended.write(false);
storage.active_at_block.initialize(context.block_number());
self.storage.admin.write(admin);
self.storage.vote_ended.write(false);
self.storage.active_at_block.initialize(self.context.block_number());
}
```

Expand All @@ -171,17 +171,17 @@ fn constructor(admin: AztecAddress) {
fn cast_vote(candidate: Field) {
// Step 1: Get nullifier public key
let msg_sender_nullifier_public_key_message_hash =
get_public_keys(context.msg_sender().unwrap()).npk_m.hash();
get_public_keys(self.msg_sender().unwrap()).npk_m.hash();

// Step 2: Get secret key (only you can do this)
let secret = context.request_nsk_app(msg_sender_nullifier_public_key_message_hash);
let secret = self.context.request_nsk_app(msg_sender_nullifier_public_key_message_hash);

// Step 3: Create nullifier (prevents double voting)
let nullifier = poseidon2_hash([context.msg_sender().unwrap().to_field(), secret]);
context.push_nullifier(nullifier);
let nullifier = poseidon2_hash([self.msg_sender().unwrap().to_field(), secret]);
self.context.push_nullifier(nullifier);

// Step 4: Add vote to public tally (in a separate public function)
PrivateVoting::at(context.this_address()).add_to_tally_public(candidate).enqueue(&mut context);
// Step 4: Add vote to public tally (using enqueue_self for same-contract calls)
self.enqueue_self.add_to_tally_public(candidate);
}
```

Expand All @@ -194,21 +194,21 @@ fn cast_vote(candidate: Field) {
**add_to_tally_public** - Public function to increment vote count

```noir
#[only_self]
#[external("public")]
#[internal]
fn add_to_tally_public(candidate: Field) {
assert(storage.vote_ended.read() == false, "Vote has ended");
let new_tally = storage.tally.at(candidate).read() + 1;
storage.tally.at(candidate).write(new_tally);
assert(self.storage.vote_ended.read() == false, "Vote has ended");
let new_tally = self.storage.tally.at(candidate).read() + 1;
self.storage.tally.at(candidate).write(new_tally);
}
```

**get_vote** - Read vote tally (unconstrained = free to call)

```noir
#[external("utility")]
unconstrained fn get_vote(candidate: Field) -> Field {
storage.tally.at(candidate).read()
unconstrained fn get_vote(candidate: Field) -> pub Field {
self.storage.tally.at(candidate).read()
}
```

Expand Down Expand Up @@ -390,8 +390,8 @@ voteButton.addEventListener('click', async (e) => {
throw new Error('No account connected');
}

// 3. Prepare contract interaction
const votingContract = await PrivateVotingContract.at(
// 3. Prepare contract interaction (Contract.at is now synchronous)
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
wallet
);
Expand All @@ -411,7 +411,7 @@ voteButton.addEventListener('click', async (e) => {

```typescript
async function updateVoteTally(wallet: Wallet, from: AztecAddress) {
const votingContract = await PrivateVotingContract.at(
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
wallet
);
Expand Down Expand Up @@ -572,10 +572,10 @@ stateDiagram-v2
- State is transparent
- Example: `add_to_tally_public()`

**Internal Functions** (`#[internal]`):
**Only-Self Functions** (`#[only_self]`):

- Can only be called from within the contract
- Example: `add_to_tally_public()` is internal and can only be called by `cast_vote()`
- Can only be called from within the same contract
- Example: `add_to_tally_public()` can only be called by `cast_vote()` via `self.enqueue_self`

**Utility Functions** (`#[external("utility")]`):

Expand Down Expand Up @@ -612,8 +612,8 @@ The `cast_vote` function uses a clever pattern:
**Prerequisites**:

```bash
# Install Aztec tools (version 3.0.0-devnet.4)
aztec-up 3.0.0-devnet.4
# Install Aztec tools (version 3.0.0-devnet.20251212)
aztec-up 3.0.0-devnet.20251212

# Install dependencies
yarn install
Expand All @@ -627,10 +627,9 @@ yarn build-contracts

What happens:

1. Noir compiler (`aztec-nargo`) compiles `contracts/src/main.nr`
2. Post-processor generates artifacts
3. TypeScript bindings are generated
4. Artifacts copied to `app/artifacts/`
1. Aztec CLI (`aztec compile`) compiles `contracts/src/main.nr`
2. TypeScript bindings are generated (`aztec codegen`)
3. Artifacts copied to `app/artifacts/`

**Step 2: Deploy Contracts**

Expand Down Expand Up @@ -740,4 +739,4 @@ PROVER_ENABLED=true # Enable/disable proving

---

_Generated for aztec-web-starter - Aztec 3.0.0-devnet.4_
_Generated for aztec-web-starter - Aztec 3.0.0-devnet.20251212_
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ This is an example web app that demonstrates how to interact with an Aztec contr

- Uses the [Private Voting](https://docs.aztec.network/developers/tutorials/codealong/contract_tutorials/private_voting_contract) example
- Includes an embedded wallet. This is only for demonstration purposes and not for production use.
- Works on top of the Sandbox, but can be adapted to work with a testnet.
- Works on top of the local network, but can be adapted to work with a testnet.

### Setup

1. Install the Aztec tools from the first few steps in [Quick Start Guide](https://docs.aztec.network/developers/getting_started).

Please note that this project uses `3.0.0-devnet.4` version of Aztec SDK. If you wish to use a different version, please update the dependencies in the `app/package.json` and in `contracts/Nargo.toml` file to match your version.
Please note that this project uses `3.0.0-devnet.20251212` version of Aztec SDK. If you wish to use a different version, please update the dependencies in the `package.json` and in `contracts/Nargo.toml` file to match your version.

You can install a specific version of Aztec tools by running `aztec-up 3.0.0-devnet.4`
You can install a specific version of Aztec tools by running `aztec-up 3.0.0-devnet.20251212`

2. Compile smart contracts in `/contracts`:

Expand All @@ -31,11 +31,11 @@ yarn install
yarn deploy-contracts
```

The deploy script generates a random account and deploys the voting contract with it. It also uses the SponsoredFPC contract for fee payment. This is sufficient for testing with Sandbox, but is not suitable for production setup.
The deploy script generates a random account and deploys the voting contract with it. It also uses the SponsoredFPC contract for fee payment. This is sufficient for testing with local network, but is not suitable for production setup.

The script also writes the deployment info to `.env` (which our web-app reads from).

> Note that the script generates client proofs and it may take a couple of seconds. For faster development, you can disable proving by calling with `PROVER_ENABLED=false` (Sandbox accepts transactions without a valid proof).
> Note that the script generates client proofs and it may take a couple of seconds. For faster development, you can disable proving by calling with `PROVER_ENABLED=false` (The local network accepts transactions without a valid proof).

4. Run the app (development mode):

Expand Down Expand Up @@ -65,7 +65,7 @@ yarn test

## Disable client proofs

The Sandbox will accept transactions without a valid proof. You can disable proof generation when working against the Sandbox as it will save time during development.
The local network will accept transactions without a valid proof. You can disable proof generation when working against the local network as it will save time during development.

To disable proving in the deploy script, run:

Expand Down
61 changes: 37 additions & 24 deletions app/embedded-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Account, SignerlessAccount } from '@aztec/aztec.js/account';
import { AztecAddress } from '@aztec/aztec.js/addresses';
import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts';
import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee';
import { Fr } from '@aztec/aztec.js/fields';
import { createLogger } from '@aztec/aztec.js/log';
import { createAztecNodeClient } from '@aztec/aztec.js/node';
import { type UserFeeOptions, type FeeOptions, BaseWallet, AccountManager, DeployAccountOptions, SimulateOptions } from '@aztec/aztec.js/wallet';
import {
AccountManager,
type DeployAccountOptions,
type SimulateOptions,
} from '@aztec/aztec.js/wallet';
import { SPONSORED_FPC_SALT } from '@aztec/constants';
import { randomBytes } from '@aztec/foundation/crypto';
import {
AccountFeePaymentMethodOptions,
type DefaultAccountEntrypointOptions,
} from '@aztec/entrypoints/account';
import { randomBytes } from '@aztec/foundation/crypto/random';
import { Fr } from '@aztec/foundation/curves/bn254';
import { EcdsaRAccountContract } from '@aztec/accounts/ecdsa/lazy';
import { SchnorrAccountContract } from '@aztec/accounts/schnorr/lazy';

Expand All @@ -21,13 +28,11 @@ import {
import {
ExecutionPayload,
mergeExecutionPayloads,
} from '@aztec/entrypoints/payload';
import { TxSimulationResult } from '@aztec/stdlib/tx';
type TxSimulationResult,
} from '@aztec/stdlib/tx';
import { GasSettings } from '@aztec/stdlib/gas';
import {
AccountFeePaymentMethodOptions,
DefaultAccountEntrypointOptions,
} from '@aztec/entrypoints/account';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { type FeeOptions, BaseWallet } from '@aztec/wallet-sdk/base-wallet';

const PROVER_ENABLED = false;

Expand Down Expand Up @@ -64,20 +69,22 @@ export class EmbeddedWallet extends BaseWallet {
* This wallet will use the sponsoredFPC payment method
* unless otherwise stated
* @param from - The address where the transaction is being sent from
* @param userFeeOptions - User-provided fee options, which might be incomplete
* @param feePayer - The address paying for fees (if embedded in the execution payload)
* @param gasSettings - User-provided partial gas settings
* @returns - Populated fee options that can be used to create a transaction execution request
*/
override async getDefaultFeeOptions(
protected override async completeFeeOptions(
from: AztecAddress,
userFeeOptions: UserFeeOptions | undefined
feePayer?: AztecAddress,
gasSettings?: Partial<GasSettings>
): Promise<FeeOptions> {
const maxFeesPerGas =
userFeeOptions?.gasSettings?.maxFeesPerGas ??
gasSettings?.maxFeesPerGas ??
(await this.aztecNode.getCurrentBaseFees()).mul(1 + this.baseFeePadding);
let walletFeePaymentMethod;
let accountFeePaymentMethodOptions;
// The transaction does not include a fee payment method, so we set a default
if (!userFeeOptions?.embeddedPaymentMethodFeePayer) {
if (!feePayer) {
const sponsoredFPCContract =
await EmbeddedWallet.#getSponsoredPFCContract();
walletFeePaymentMethod = new SponsoredFeePaymentMethod(
Expand All @@ -87,19 +94,17 @@ export class EmbeddedWallet extends BaseWallet {
} else {
// The transaction includes fee payment method, so we check if we are the fee payer for it
// (this can only happen if the embedded payment method is FeeJuiceWithClaim)
accountFeePaymentMethodOptions = from.equals(
userFeeOptions.embeddedPaymentMethodFeePayer
)
accountFeePaymentMethodOptions = from.equals(feePayer)
? AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM
: AccountFeePaymentMethodOptions.EXTERNAL;
}
const gasSettings: GasSettings = GasSettings.default({
...userFeeOptions?.gasSettings,
const fullGasSettings: GasSettings = GasSettings.default({
...gasSettings,
maxFeesPerGas,
});
this.log.debug(`Using L2 gas settings`, gasSettings);
this.log.debug(`Using L2 gas settings`, fullGasSettings);
return {
gasSettings,
gasSettings: fullGasSettings,
walletFeePaymentMethod,
accountFeePaymentMethodOptions,
};
Expand Down Expand Up @@ -313,8 +318,16 @@ export class EmbeddedWallet extends BaseWallet {
opts: SimulateOptions
): Promise<TxSimulationResult> {
const feeOptions = opts.fee?.estimateGas
? await this.getFeeOptionsForGasEstimation(opts.from, opts.fee)
: await this.getDefaultFeeOptions(opts.from, opts.fee);
? await this.completeFeeOptionsForEstimation(
opts.from,
executionPayload.feePayer,
opts.fee?.gasSettings
)
: await this.completeFeeOptions(
opts.from,
executionPayload.feePayer,
opts.fee?.gasSettings
);
const feeExecutionPayload =
await feeOptions.walletFeePaymentMethod?.getExecutionPayload();
const executionOptions: DefaultAccountEntrypointOptions = {
Expand Down
8 changes: 4 additions & 4 deletions app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ createAccountButton.addEventListener('click', async (e) => {
});

// Connect a test account
// Sandbox comes with some test accounts. This can be used instead of creating new ones
// when building against the Sandbox.
// Local network comes with some test accounts. This can be used instead of creating new ones
// when building against the local network.
connectTestAccountButton.addEventListener('click', async (e) => {
e.preventDefault();
const button = e.target as HTMLButtonElement;
Expand Down Expand Up @@ -147,7 +147,7 @@ voteButton.addEventListener('click', async (e) => {
}

// Prepare contract interaction
const votingContract = await PrivateVotingContract.at(
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
wallet
);
Expand Down Expand Up @@ -181,7 +181,7 @@ async function updateVoteTally(wallet: Wallet, from: AztecAddress) {
displayStatusMessage('Updating vote tally...');

// Prepare contract interaction
const votingContract = await PrivateVotingContract.at(
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
wallet
);
Expand Down
2 changes: 1 addition & 1 deletion contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name = "private_voting"
type = "contract"

[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="v3.0.0-devnet.4", directory="noir-projects/aztec-nr/aztec" }
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="v3.0.0-devnet.20251212", directory="noir-projects/aztec-nr/aztec" }
Loading
Loading