From 25f6771c8e223f0410acdf587fd609b7d55e1cd7 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Mon, 20 Apr 2026 22:02:22 +0100 Subject: [PATCH 01/14] fix(pxe): propagate calldata count from nested private oracles (backport #22642) Cherry-pick commit with conflict markers preserved. --- noir-projects/noir-contracts/Nargo.toml | 1 + .../calldata_limit_test_contract/Nargo.toml | 8 +++++ .../calldata_limit_test_contract/src/main.nr | 32 +++++++++++++++++++ .../oracle/private_execution.test.ts | 19 +++++++++++ .../oracle/private_execution_oracle.ts | 7 ++++ 5 files changed, 67 insertions(+) create mode 100644 noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/src/main.nr diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index aebfcfdf6dd8..8cf4c0d0c45e 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -42,6 +42,7 @@ members = [ "contracts/test/avm_initializer_test_contract", "contracts/test/avm_test_contract", "contracts/test/benchmarking_contract", + "contracts/test/calldata_limit_test_contract", "contracts/test/child_contract", "contracts/test/counter/counter_contract", "contracts/test/custom_message_contract", diff --git a/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/Nargo.toml new file mode 100644 index 000000000000..e26e9c5afd5e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "calldata_limit_test_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/src/main.nr new file mode 100644 index 000000000000..026005b35137 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/calldata_limit_test_contract/src/main.nr @@ -0,0 +1,32 @@ +use aztec::macros::aztec; + +// Test contract covering behavior around the MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS limit. +#[aztec] +pub contract CalldataLimitTest { + use aztec::macros::functions::{external, only_self}; + use aztec::protocol::constants::MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS; + + // Each call contributes MAX/3 args + 1 function-selector field. Three such calls total MAX + 3, over the limit. + #[external("public")] + #[only_self] + fn public_call_with_third_of_max_args( + _args: [Field; MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS / 3], + ) {} + + // Enqueues three public calls split across a parent/nested-private-call boundary: one from the parent, one from + // the nested call, and one more from the parent after the nested call returns. Expected to fail during private + // simulation: each individual frame's calldata total stays under the limit, but the tree-wide total is over. + #[external("private")] + fn exceed_calldata_limit_via_nested_call() { + let args = [0; MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS / 3]; + self.enqueue_self.public_call_with_third_of_max_args(args); + self.call_self.enqueue_one_public_call(); + self.enqueue_self.public_call_with_third_of_max_args(args); + } + + #[external("private")] + fn enqueue_one_public_call() { + let args = [0; MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS / 3]; + self.enqueue_self.public_call_with_third_of_max_args(args); + } +} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 18e32c9005dc..65cf150eeac1 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -17,8 +17,12 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { type Logger, createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; import { KeyStore } from '@aztec/key-store'; +<<<<<<< HEAD import { openTmpStore } from '@aztec/kv-store/lmdb'; import { type AppendOnlyTree, Poseidon, StandardTree, newTree } from '@aztec/merkle-tree'; +======= +import { CalldataLimitTestContractArtifact } from '@aztec/noir-test-contracts.js/CalldataLimitTest'; +>>>>>>> 1e6db27f0d (fix(pxe): propagate calldata count from nested private oracles (#22642)) import { ChildContractArtifact } from '@aztec/noir-test-contracts.js/Child'; import { ParentContractArtifact } from '@aztec/noir-test-contracts.js/Parent'; import { PendingNoteHashesContractArtifact } from '@aztec/noir-test-contracts.js/PendingNoteHashes'; @@ -1086,6 +1090,21 @@ describe('Private Execution test suite', () => { }), ).rejects.toThrow(/Too many total args to all enqueued public calls/); }); + + it('should error if parent and nested private call enqueue public calls with too many TOTAL args', async () => { + const contractArtifact = structuredClone(CalldataLimitTestContractArtifact); + const contractAddress = await mockContractInstance(contractArtifact); + + await expect( + runSimulator({ + msgSender: contractAddress, + contractAddress: contractAddress, + anchorBlockHeader, + artifact: contractArtifact, + functionName: 'exceed_calldata_limit_via_nested_call', + }), + ).rejects.toThrow(/Too many total args to all enqueued public calls/); + }); }); describe('setting teardown function', () => { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 4899899afb56..74122a9c4668 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -593,6 +593,9 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP functionSelector, ); + // Propagate the nested call's calldata count so the parent sees its increments on subsequent enqueues. + this.totalPublicCalldataCount = privateExecutionOracle.getTotalPublicCalldataCount(); + if (isStaticCall) { this.#checkValidStaticCall(childExecutionResult); } @@ -626,6 +629,10 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP return Promise.resolve(); } + public getTotalPublicCalldataCount(): number { + return this.totalPublicCalldataCount; + } + public notifyRevertiblePhaseStart(minRevertibleSideEffectCounter: number): Promise { return this.noteCache.setMinRevertibleSideEffectCounter(minRevertibleSideEffectCounter); } From c3bb53848bda2c9616a4adb65ae76208e4391890 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Mon, 20 Apr 2026 21:06:04 +0000 Subject: [PATCH 02/14] fix: resolve cherry-pick conflicts --- .../oracle/private_execution.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 65cf150eeac1..0dd6904fc9fe 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -17,12 +17,9 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { type Logger, createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; import { KeyStore } from '@aztec/key-store'; -<<<<<<< HEAD import { openTmpStore } from '@aztec/kv-store/lmdb'; import { type AppendOnlyTree, Poseidon, StandardTree, newTree } from '@aztec/merkle-tree'; -======= import { CalldataLimitTestContractArtifact } from '@aztec/noir-test-contracts.js/CalldataLimitTest'; ->>>>>>> 1e6db27f0d (fix(pxe): propagate calldata count from nested private oracles (#22642)) import { ChildContractArtifact } from '@aztec/noir-test-contracts.js/Child'; import { ParentContractArtifact } from '@aztec/noir-test-contracts.js/Parent'; import { PendingNoteHashesContractArtifact } from '@aztec/noir-test-contracts.js/PendingNoteHashes'; From 0902d2f48c9f59fc3134304c39a6afb215d82ea9 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Mon, 20 Apr 2026 22:07:21 +0000 Subject: [PATCH 03/14] chore: retry CI (prior failure was unrelated flaky test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit epochs_l1_reorgs.parallel.test.ts 'updates L1 to L2 messages changed due to an L1 reorg' — known flake in e2e-p2p-epoch-flakes group, unrelated to this backport. From 3c007ed95f353914df292a960d6afe2dbbbe5b6c Mon Sep 17 00:00:00 2001 From: danielntmd <162406516+danielntmd@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:52:19 -0500 Subject: [PATCH 04/14] fix: (A-589) epochs l1 reorgs test (#20999) Use same structure as the handles missed message inserted by an L1 reorg test to wait for checkpoint when sending L2 txs to help trigger mbps. Co-authored-by: danielntmd --- .../src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts index 1e56cfb770a8..13b9f9b7207b 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts @@ -442,8 +442,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { ); it('updates L1 to L2 messages changed due to an L1 reorg', async () => { - // Send L2 txs to trigger multi-block checkpoints + // Send L2 txs to trigger multi-block checkpoints and wait for them to land in a checkpoint await sendTransactions(TX_COUNT, 100); + await test.waitUntilCheckpointNumber(CheckpointNumber(2), L2_SLOT_DURATION_IN_S * 4); // Send 3 messages and wait for archiver sync logger.warn(`Sending 3 cross chain messages`); From f408c05716f988d0f4b5b644e2d26253c39e4f0b Mon Sep 17 00:00:00 2001 From: David Banks <47112877+dbanks12@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:19:27 -0400 Subject: [PATCH 05/14] docs: fix issues with Creating Accounts page (backport #22673) Cherry-pick of 1c4aa0cb0e9668d31807b51afefbf4c2061d28c5 from PR #22673. Conflict markers preserved AS-IS in this commit; resolved in follow-up. Original PR description: ## Summary Three issues on `/developers/docs/aztec-js/how_to_create_account` that block users following the guide end-to-end. **1. Install command missing `@aztec/noir-contracts.js`.** The Sponsored FPC snippet imports `@aztec/noir-contracts.js/SponsoredFPC`, but the Install dependencies section only lists `@aztec/aztec.js` and `@aztec/wallets`. **2. Misleading info-note wording.** Reworded info note below the Sponsored FPC snippet. **3. Invisible `feeJuiceAccount` in Verify deployment.** Wrap the second account creation in a new `docs:start:create_fee_juice_account` block; switch `verify_account_deployment` to reference `newAccount`. --- .../docs/aztec-js/how_to_create_account.md | 32 +++++++++++++++++-- .../docs/aztec-js/how_to_create_account.md | 16 ++++++++-- docs/examples/ts/aztecjs_connection/index.ts | 13 ++++++-- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md index ce7d00a72479..69cb875f8a18 100644 --- a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md +++ b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md @@ -15,7 +15,11 @@ This guide shows you how to create and deploy a new account on Aztec. ## Install dependencies ```bash +<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md yarn add @aztec/aztec.js@4.2.0-aztecnr-rc.2 @aztec/wallets@4.2.0-aztecnr-rc.2 +======= +yarn add @aztec/aztec.js@4.2.0 @aztec/wallets@4.2.0 @aztec/noir-contracts.js@4.2.0 +>>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-js/how_to_create_account.md ``` ## Create a new account @@ -78,12 +82,32 @@ await deployMethod.send({ :::info +<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md See the [guide on fees](./how_to_pay_fees.md#sponsored-fee-payment-contracts) for setting up the Sponsored FPC. +======= +See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. +>>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-js/how_to_create_account.md ::: ### Using Fee Juice -If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridge-fee-juice-from-l1), you can claim it and deploy in one step using `FeeJuicePaymentMethodWithClaim`: +If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridge-fee-juice-from-l1), you can claim it and deploy in one step using `FeeJuicePaymentMethodWithClaim`. + +Create a new Schnorr account for this path: + +```typescript title="create_fee_juice_account" showLineNumbers +// `feeJuiceAccount` is just another Schnorr account — the same kind as +// `newAccount` above. It gets its own name here so both deploy paths +// can coexist in one example; in your own code, pick whichever name fits. +const feeJuiceSecret = Fr.random(); +const feeJuiceSalt = Fr.random(); +const feeJuiceAccount = await wallet.createSchnorrAccount( + feeJuiceSecret, + feeJuiceSalt, +); +``` + +Claim the bridged Fee Juice and deploy in one step: ```typescript title="bridge_fee_juice_claim" showLineNumbers import { FeeJuicePaymentMethodWithClaim } from "@aztec/aztec.js/fee"; @@ -106,10 +130,12 @@ The `from: NO_FROM` signals that this transaction should be executed without acc ## Verify deployment -Confirm the account was deployed successfully: +Confirm the account was deployed successfully. Substitute the account variable for whichever path you used above (`newAccount` for the Sponsored FPC path, `feeJuiceAccount` for the Fee Juice path): ```typescript title="verify_account_deployment" showLineNumbers -const metadata = await wallet.getContractMetadata(feeJuiceAccount.address); +// `newAccount` refers to whichever account you just deployed — +// either the Sponsored FPC account or `feeJuiceAccount` from the Fee Juice path. +const metadata = await wallet.getContractMetadata(newAccount.address); console.log("Account deployed:", metadata.initializationStatus); ``` > Source code: docs/examples/ts/aztecjs_connection/index.ts#L157-L160 diff --git a/docs/docs-developers/docs/aztec-js/how_to_create_account.md b/docs/docs-developers/docs/aztec-js/how_to_create_account.md index cc1de15a2786..4520bb9d8d08 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_create_account.md +++ b/docs/docs-developers/docs/aztec-js/how_to_create_account.md @@ -15,7 +15,7 @@ This guide shows you how to create and deploy a new account on Aztec. ## Install dependencies ```bash -yarn add @aztec/aztec.js@#include_version_without_prefix @aztec/wallets@#include_version_without_prefix +yarn add @aztec/aztec.js@#include_version_without_prefix @aztec/wallets@#include_version_without_prefix @aztec/noir-contracts.js@#include_version_without_prefix ``` ## Create a new account @@ -41,12 +41,22 @@ If your account doesn't have Fee Juice, use the [Sponsored FPC](./how_to_pay_fee #include_code deploy_account_sponsored_fpc /docs/examples/ts/aztecjs_connection/index.ts typescript :::info +<<<<<<< HEAD See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc-devnet-and-local-only) for setting up the Sponsored FPC. +======= +See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. +>>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)) ::: ### Using Fee Juice -If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridge-fee-juice-from-l1), you can claim it and deploy in one step using `FeeJuicePaymentMethodWithClaim`: +If your account has Fee Juice from a [bridge from L1](./how_to_pay_fees.md#bridge-fee-juice-from-l1), you can claim it and deploy in one step using `FeeJuicePaymentMethodWithClaim`. + +Create a new Schnorr account for this path: + +#include_code create_fee_juice_account /docs/examples/ts/aztecjs_connection/index.ts typescript + +Claim the bridged Fee Juice and deploy in one step: #include_code bridge_fee_juice_claim /docs/examples/ts/aztecjs_connection/index.ts typescript @@ -56,7 +66,7 @@ The `from: NO_FROM` signals that this transaction should be executed without acc ## Verify deployment -Confirm the account was deployed successfully: +Confirm the account was deployed successfully. Substitute the account variable for whichever path you used above (`newAccount` for the Sponsored FPC path, `feeJuiceAccount` for the Fee Juice path): #include_code verify_account_deployment /docs/examples/ts/aztecjs_connection/index.ts typescript diff --git a/docs/examples/ts/aztecjs_connection/index.ts b/docs/examples/ts/aztecjs_connection/index.ts index 76b86a316c9f..c774d1680c20 100644 --- a/docs/examples/ts/aztecjs_connection/index.ts +++ b/docs/examples/ts/aztecjs_connection/index.ts @@ -81,13 +81,17 @@ await deployMethod.send({ }); // docs:end:deploy_account_sponsored_fpc -// Create a separate account to deploy with Fee Juice bridged from L1 +// docs:start:create_fee_juice_account +// `feeJuiceAccount` is just another Schnorr account — the same kind as +// `newAccount` above. It gets its own name here so both deploy paths +// can coexist in one example; in your own code, pick whichever name fits. const feeJuiceSecret = Fr.random(); const feeJuiceSalt = Fr.random(); const feeJuiceAccount = await wallet.createSchnorrAccount( feeJuiceSecret, feeJuiceSalt, ); +// docs:end:create_fee_juice_account // docs:start:bridge_fee_juice_setup import { createExtendedL1Client } from "@aztec/ethereum/client"; @@ -165,6 +169,11 @@ await deployMethodBridged.send({ // docs:end:bridge_fee_juice_claim // docs:start:verify_account_deployment -const metadata = await wallet.getContractMetadata(feeJuiceAccount.address); +// `newAccount` refers to whichever account you just deployed — +// either the Sponsored FPC account or `feeJuiceAccount` from the Fee Juice path. +const metadata = await wallet.getContractMetadata(newAccount.address); console.log("Account deployed:", metadata.initializationStatus); // docs:end:verify_account_deployment + +const feeJuiceMetadata = await wallet.getContractMetadata(feeJuiceAccount.address); +console.log("Fee Juice account deployed:", feeJuiceMetadata.initializationStatus); From b7123e0aa00622c6f1d6624bff765294e93c2fc4 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 21 Apr 2026 14:25:54 +0000 Subject: [PATCH 06/14] fix: resolve cherry-pick conflicts - Renamed v4.2.0 versioned docs path to v4.2.0-aztecnr-rc.2 (already done by automerge). - Install command: kept 4.2.0-aztecnr-rc.2 version suffix while adding the new @aztec/noir-contracts.js dependency from PR #22673. - Sponsored-FPC info note: kept the existing per-branch fragment anchors (#sponsored-fee-payment-contracts in the versioned doc, #sponsored-fpc-devnet-and-local-only in docs-developers) while adopting the PR's reworded sentence. --- .../docs/aztec-js/how_to_create_account.md | 12 ++---------- .../docs/aztec-js/how_to_create_account.md | 6 +----- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md index 69cb875f8a18..80e8be3ffa6d 100644 --- a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md +++ b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md @@ -15,11 +15,7 @@ This guide shows you how to create and deploy a new account on Aztec. ## Install dependencies ```bash -<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md -yarn add @aztec/aztec.js@4.2.0-aztecnr-rc.2 @aztec/wallets@4.2.0-aztecnr-rc.2 -======= -yarn add @aztec/aztec.js@4.2.0 @aztec/wallets@4.2.0 @aztec/noir-contracts.js@4.2.0 ->>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-js/how_to_create_account.md +yarn add @aztec/aztec.js@4.2.0-aztecnr-rc.2 @aztec/wallets@4.2.0-aztecnr-rc.2 @aztec/noir-contracts.js@4.2.0-aztecnr-rc.2 ``` ## Create a new account @@ -82,11 +78,7 @@ await deployMethod.send({ :::info -<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-js/how_to_create_account.md -See the [guide on fees](./how_to_pay_fees.md#sponsored-fee-payment-contracts) for setting up the Sponsored FPC. -======= -See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. ->>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-js/how_to_create_account.md +See the [guide on fees](./how_to_pay_fees.md#sponsored-fee-payment-contracts) for more details on the Sponsored FPC and what this snippet means. ::: ### Using Fee Juice diff --git a/docs/docs-developers/docs/aztec-js/how_to_create_account.md b/docs/docs-developers/docs/aztec-js/how_to_create_account.md index 4520bb9d8d08..65fcb951ac4e 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_create_account.md +++ b/docs/docs-developers/docs/aztec-js/how_to_create_account.md @@ -41,11 +41,7 @@ If your account doesn't have Fee Juice, use the [Sponsored FPC](./how_to_pay_fee #include_code deploy_account_sponsored_fpc /docs/examples/ts/aztecjs_connection/index.ts typescript :::info -<<<<<<< HEAD -See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc-devnet-and-local-only) for setting up the Sponsored FPC. -======= -See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc) for more details on the Sponsored FPC and what this snippet means. ->>>>>>> 1c4aa0cb0e (docs: fix issues with Creating Accounts page (#22673)) +See the [guide on fees](./how_to_pay_fees.md#sponsored-fpc-devnet-and-local-only) for more details on the Sponsored FPC and what this snippet means. ::: ### Using Fee Juice From 13db1521e15dec654d478a47a08b78679cd79852 Mon Sep 17 00:00:00 2001 From: David Banks <47112877+dbanks12@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:19:15 -0400 Subject: [PATCH 07/14] cherry-pick: docs: fix v4.2.0 counter tutorial and debug logging references (#22668) (with conflicts) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #22668 (squash merge commit 34a28d4241) onto v4-next. Conflicts + git rename-detection misfires to resolve in next commit: - version-v4.1.0-rc.2/docs/aztec-nr/debugging.md: auto-merged from version-v4.2.0/... — git mapped the v4.2.0 patch here because version-v4.2.0/ does not exist on v4-next. Out of original PR scope (the original author intentionally only patched v4.2.0/). - version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md: same — auto-merged from v4.2.0 misfire. Out of original PR scope. - docs/docs-developers/docs/aztec-nr/logging.md: file does not exist on v4-next (the unversioned source still uses debugging.md here — the rename to logging.md hasn't happened on this branch yet). Cherry-pick left the modified version of logging.md in tree, but it should be dropped and the equivalent LOG_LEVEL clarification applied to debugging.md instead in a follow-up commit. Legitimate portion: docs-developers/docs/tutorials/contract_tutorials/counter_contract.md oracle::debug_log -> oracle::logging bullet fix (auto-applied cleanly). --- .../docs/aztec-nr/debugging.md | 9 +- .../contract_tutorials/counter_contract.md | 4 +- docs/docs-developers/docs/aztec-nr/logging.md | 239 ++++++++++++++++++ .../contract_tutorials/counter_contract.md | 2 +- 4 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 docs/docs-developers/docs/aztec-nr/logging.md diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md index d5af37e48e04..98e6d635e922 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md @@ -33,7 +33,7 @@ Log values from your contract using `debug_log`: ```rust // Import debug logging -use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format }; +use dep::aztec::oracle::logging::{ debug_log, debug_log_format }; // Log simple messages debug_log("checkpoint reached"); @@ -71,6 +71,8 @@ LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_ - `level:module` - Sets level for a specific module - `level:module:submodule` - Sets level for a specific submodule +**The default-level filter must be the first segment.** A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level`, because the parser reads everything before the first `;` as the default level. To filter only specific modules, lead with a default level — use `silent` to suppress everything else. + ```bash # Default level only LOG_LEVEL="debug" @@ -80,6 +82,9 @@ LOG_LEVEL="info;debug:simulator;debug:execution" # Default level + specific submodule overrides LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_view_context" + +# Silence everything except one module +LOG_LEVEL="silent;debug:simulator" ``` ::: @@ -207,7 +212,7 @@ LOG_LEVEL=verbose aztec start --local-network ### Common debug imports ```rust -use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format }; +use dep::aztec::oracle::logging::{ debug_log, debug_log_format }; ``` ### Check contract registration diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md index 15c73bdb3285..12c0602d2d28 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md @@ -22,7 +22,7 @@ This tutorial is compatible with the Aztec version `v4.1.0-rc.2`. Install the co Run this to create a new contract project: ```bash -aztec new --contract counter +aztec new counter ``` Your structure should look like this: @@ -90,7 +90,7 @@ use balance_set::BalanceSet; - `messages::message_delivery::MessageDelivery` Imports `MessageDelivery` for specifying how note delivery should be handled (e.g., constrained onchain delivery). -- `oracle::debug_log::debug_log_format` +- `oracle::logging::debug_log_format` Imports a debug logging utility for printing formatted messages during contract execution. - `protocol::{address::AztecAddress, traits::ToField}` diff --git a/docs/docs-developers/docs/aztec-nr/logging.md b/docs/docs-developers/docs/aztec-nr/logging.md new file mode 100644 index 000000000000..603a361432c3 --- /dev/null +++ b/docs/docs-developers/docs/aztec-nr/logging.md @@ -0,0 +1,239 @@ +--- +title: Logging from Contracts +sidebar_position: 4 +tags: [contracts, logging, debugging, aztec.nr] +description: Add log statements to your Aztec contracts and control log verbosity in tests and local networks. +--- + +Aztec contracts can emit log messages at seven severity levels. Private function logs appear immediately during local simulation in the Private eXecution Environment (PXE), while public function logs are collected and displayed in test mode. + +## Prerequisites + +- An Aztec contract project set up with the `aztec-nr` dependency +- Basic understanding of [private, public, and utility functions](./framework-description/functions/visibility.md) + +## Import the logging functions + +Logging functions live under `aztec::oracle::logging`. Import the specific functions you need: + +#include_code logging_imports /docs/examples/contracts/logging_example/src/main.nr rust + +Or import only what you need: + +```rust +use aztec::oracle::logging::{info_log, debug_log, debug_log_format}; +``` + +:::warning Old import path removed +The previous import path `dep::aztec::oracle::debug_log` has been removed. Update your imports to use `aztec::oracle::logging` instead. +::: + +## Log levels + +Aztec supports seven log levels, ordered from least to most verbose: + +| Level | Value | When to use | +|---------|-------|-------------| +| `fatal` | 1 | Unrecoverable errors that should always be visible | +| `error` | 2 | Recoverable errors or unexpected conditions | +| `warn` | 3 | Potential issues worth investigating | +| `info` | 4 | General operational information | +| `verbose` | 5 | Detailed information for troubleshooting | +| `debug` | 6 | Development-time debugging output | +| `trace` | 7 | Fine-grained tracing of execution flow | + +When you set `LOG_LEVEL=info`, you see `fatal`, `error`, `warn`, and `info` messages, but `verbose`, `debug`, and `trace` are hidden. + +Here is an example using all seven levels: + +#include_code log_all_levels /docs/examples/contracts/logging_example/src/main.nr rust + +## Simple log messages + +Each level has a function that accepts a plain string with no format arguments: + +#include_code log_simple /docs/examples/contracts/logging_example/src/main.nr rust + +## Log messages with format arguments + +Each level also has a `_format` variant that accepts a format string and an array of `Field` values. Use `{0}`, `{1}`, etc. to insert individual arguments by index, or `{}` to print the entire array: + +#include_code log_format_patterns /docs/examples/contracts/logging_example/src/main.nr rust + +:::note +Format arguments must be `Field` values. Use `.to_field()` to convert addresses and other types: + +#include_code log_address /docs/examples/contracts/logging_example/src/main.nr rust +::: + +## Viewing logs + +### In `aztec test` (Noir tests) + +To see contract logs, set `LOG_LEVEL` to include the `debug_log` module: + +```bash +LOG_LEVEL="error;trace:debug_log" aztec test +``` + +:::tip +Use different log levels strategically: add `info_log` calls for key state transitions you always want to see, and `debug_log` or `trace_log` calls for detailed inspection. +::: + +### In TypeScript tests (jest, vitest) + +TypeScript test environments do not enable contract logs by default. Set the `LOG_LEVEL` environment variable to include the `contract_log` module: + +```bash +# Show contract logs at debug level and above +LOG_LEVEL="info;debug:contract_log" yarn test + +# Show all contract log levels (most verbose) +LOG_LEVEL="error;trace:contract_log" yarn test +``` + +### With a local network + +Contract logs appear in the process that runs the PXE — your test process, not the network process. The network window only shows system-level infrastructure logs (archiver, world-state, etc.), which are generally not useful for contract debugging. + +When running TypeScript tests against a local network, set `LOG_LEVEL` on the **test command**: + +```bash +# Your test process sees contract logs +LOG_LEVEL="error;trace:contract_log" yarn test +``` + +You do not need to change the `LOG_LEVEL` on `aztec start --local-network` to see contract logs. + +## `LOG_LEVEL` syntax reference + +The `LOG_LEVEL` environment variable uses a semicolon-delimited format: + +``` +;:,;: +``` + +- **First segment (required)**: the default log level for all modules. A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level` — the parser always reads the segment before the first `;` as the default level. To filter only specific modules, start with `silent` (e.g. `LOG_LEVEL="silent;debug:simulator"`) +- **Remaining segments**: `level:module` pairs that override the default for specific modules +- Modules are comma-separated within a segment +- The `aztec:` prefix is automatically stripped from module names +- Module names support regex or prefix matching + +### Common configurations + +| Scenario | `LOG_LEVEL` value | +|----------|-------------------| +| Contract logs in `aztec test` (TXE) | `error;trace:debug_log` | +| Contract logs in TypeScript tests (PXE) | `error;trace:contract_log` | +| Contract debug+ logs with system info | `info;debug:contract_log` | +| Only contract warnings and errors | `warn;warn:contract_log` | +| Everything verbose | `verbose` | +| Debug a specific system module | `info;debug:sequencer` | +| Multiple module overrides | `warn;debug:sequencer,archiver;trace:contract_log` | + +## How contract logs are displayed + +### In `aztec test` (TXE) + +Contract logs appear under the `debug_log` module: + +``` +[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here +``` + +### In TypeScript tests (PXE) + +When running through the PXE, the output includes the contract name with an abbreviated address: + +``` +[07:40:29.937] INFO: contract_log::Counter(0x1234abcd) 164f9c87bca0cf8c Transfer completed +[07:40:29.947] DEBUG: contract_log::Counter(0x1234abcd) 164f9c87bca0cf8c Processing value: 0x2a +``` + +The hex value after the address (`164f9c87bca0cf8c`) is an internal request identifier. If the contract name cannot be resolved, you see `Unknown` in its place. + +## Logging in public functions + +Private and public functions handle logging differently: + +- **Private functions** execute locally in the PXE. You see logs immediately during simulation. +- **Public functions** execute on the sequencer. You see logs during `.simulate()` calls and after `.send().wait()` completes in test mode. + +The logging API is the same in public functions: + +#include_code log_public /docs/examples/contracts/logging_example/src/main.nr rust + +### Accessing logs programmatically + +In test mode (when not using real proofs), you can access public function debug logs on the `TxReceipt`: + +```typescript +import { applyStringFormatting } from '@aztec/foundation/log'; + +const { receipt } = await contract.methods.myPublicFunction(args).send({ + from: address, + fee: { paymentMethod }, + wait: { timeout: 600 }, +}); + +// Logs are automatically printed to your console. +// You can also access them programmatically: +if (receipt.debugLogs) { + for (const log of receipt.debugLogs) { + console.log(`[${log.level}] ${applyStringFormatting(log.message, log.fields)}`); + } +} +``` + +Each entry contains: + +- `contractAddress` - the contract that emitted the log +- `level` - the log level (`info`, `debug`, etc.) +- `message` - the unformatted message string +- `fields` - the raw `Field` values passed as arguments + +:::warning +`receipt.debugLogs` is only available in test mode (when not using real proofs). In production, debug log collection is disabled. +::: + +## Verify logging works + +Add a `debug_log` call to any contract function, then run with logging enabled: + +```bash +LOG_LEVEL="error;trace:debug_log" aztec test +``` + +You should see output like: + +``` +[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here +``` + +If no output appears, check the troubleshooting section below. + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| No contract logs appear in `aztec test` | Set `LOG_LEVEL` to include `debug_log`, e.g., `LOG_LEVEL="error;trace:debug_log" aztec test`. Also verify you are calling a log function inside the contract function being tested. | +| No contract logs in TypeScript tests | Set `LOG_LEVEL` to include `contract_log`, e.g., `LOG_LEVEL="error;trace:contract_log" yarn test`. | +| Import error on `dep::aztec::oracle::debug_log` | This path was removed. Update to `use aztec::oracle::logging::{debug_log, debug_log_format};`. | +| `receipt.debugLogs` is `undefined` | Debug logs are only collected in test mode (non-real-proofs). They are not available in production. | +| Too much noise in log output | Narrow the default level and use module filters, e.g., `LOG_LEVEL="error;debug:contract_log"`. | + +## Quick reference + +| Task | Code or command | +|------|-----------------| +| Import logging | `use aztec::oracle::logging::{debug_log, debug_log_format};` | +| Simple log | `debug_log("message");` | +| Log with values | `debug_log_format("val: {0}", [my_field]);` | +| Run Noir tests with logs | `LOG_LEVEL="error;trace:debug_log" aztec test` | +| JS tests with contract logs | `LOG_LEVEL="error;trace:contract_log" yarn test` | + +## Next steps + +- [Debugging Aztec Code](./debugging.md) for error codes, profiling, and common issues +- [Events and Logs](./framework-description/events_and_logs.md) for emitting events that offchain applications can consume +- [Testing Contracts](./testing_contracts.md) for writing and running contract tests diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md index 39fe64bd857f..40e7bc702c08 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md @@ -82,7 +82,7 @@ pub contract Counter { - `messages::message_delivery::MessageDelivery` Imports `MessageDelivery` for specifying how note delivery should be handled (e.g., constrained onchain delivery). -- `oracle::debug_log::debug_log_format` +- `oracle::logging::debug_log_format` Imports a debug logging utility for printing formatted messages during contract execution. - `protocol::{address::AztecAddress, traits::ToField}` From 3f672312fdf9511c2cd96fa75cb100fbca7c8104 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 21 Apr 2026 14:26:58 +0000 Subject: [PATCH 08/14] fix: resolve cherry-pick conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the auto-merged changes that landed outside the original PR's scope: - version-v4.1.0-rc.2/docs/aztec-nr/debugging.md: revert. The original PR intentionally only patched the v4.2.0 versioned snapshot. v4.1.0-rc.2 is a separate published snapshot and is not within scope. - version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md: revert (same reason). - docs/docs-developers/docs/aztec-nr/logging.md: drop. This file does not exist on v4-next — the unversioned source still uses debugging.md. The cherry-pick left the modified version in tree by accident. The equivalent LOG_LEVEL clarification will be applied to debugging.md in the follow-up adapt commit. The legitimate change to docs-developers/docs/tutorials/contract_tutorials/counter_contract.md (oracle::debug_log -> oracle::logging bullet) is kept. --- .../docs/aztec-nr/debugging.md | 9 +- .../contract_tutorials/counter_contract.md | 4 +- docs/docs-developers/docs/aztec-nr/logging.md | 239 ------------------ 3 files changed, 4 insertions(+), 248 deletions(-) delete mode 100644 docs/docs-developers/docs/aztec-nr/logging.md diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md index 98e6d635e922..d5af37e48e04 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/debugging.md @@ -33,7 +33,7 @@ Log values from your contract using `debug_log`: ```rust // Import debug logging -use dep::aztec::oracle::logging::{ debug_log, debug_log_format }; +use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format }; // Log simple messages debug_log("checkpoint reached"); @@ -71,8 +71,6 @@ LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_ - `level:module` - Sets level for a specific module - `level:module:submodule` - Sets level for a specific submodule -**The default-level filter must be the first segment.** A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level`, because the parser reads everything before the first `;` as the default level. To filter only specific modules, lead with a default level — use `silent` to suppress everything else. - ```bash # Default level only LOG_LEVEL="debug" @@ -82,9 +80,6 @@ LOG_LEVEL="info;debug:simulator;debug:execution" # Default level + specific submodule overrides LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_view_context" - -# Silence everything except one module -LOG_LEVEL="silent;debug:simulator" ``` ::: @@ -212,7 +207,7 @@ LOG_LEVEL=verbose aztec start --local-network ### Common debug imports ```rust -use dep::aztec::oracle::logging::{ debug_log, debug_log_format }; +use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format }; ``` ### Check contract registration diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md index 12c0602d2d28..15c73bdb3285 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/tutorials/contract_tutorials/counter_contract.md @@ -22,7 +22,7 @@ This tutorial is compatible with the Aztec version `v4.1.0-rc.2`. Install the co Run this to create a new contract project: ```bash -aztec new counter +aztec new --contract counter ``` Your structure should look like this: @@ -90,7 +90,7 @@ use balance_set::BalanceSet; - `messages::message_delivery::MessageDelivery` Imports `MessageDelivery` for specifying how note delivery should be handled (e.g., constrained onchain delivery). -- `oracle::logging::debug_log_format` +- `oracle::debug_log::debug_log_format` Imports a debug logging utility for printing formatted messages during contract execution. - `protocol::{address::AztecAddress, traits::ToField}` diff --git a/docs/docs-developers/docs/aztec-nr/logging.md b/docs/docs-developers/docs/aztec-nr/logging.md deleted file mode 100644 index 603a361432c3..000000000000 --- a/docs/docs-developers/docs/aztec-nr/logging.md +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: Logging from Contracts -sidebar_position: 4 -tags: [contracts, logging, debugging, aztec.nr] -description: Add log statements to your Aztec contracts and control log verbosity in tests and local networks. ---- - -Aztec contracts can emit log messages at seven severity levels. Private function logs appear immediately during local simulation in the Private eXecution Environment (PXE), while public function logs are collected and displayed in test mode. - -## Prerequisites - -- An Aztec contract project set up with the `aztec-nr` dependency -- Basic understanding of [private, public, and utility functions](./framework-description/functions/visibility.md) - -## Import the logging functions - -Logging functions live under `aztec::oracle::logging`. Import the specific functions you need: - -#include_code logging_imports /docs/examples/contracts/logging_example/src/main.nr rust - -Or import only what you need: - -```rust -use aztec::oracle::logging::{info_log, debug_log, debug_log_format}; -``` - -:::warning Old import path removed -The previous import path `dep::aztec::oracle::debug_log` has been removed. Update your imports to use `aztec::oracle::logging` instead. -::: - -## Log levels - -Aztec supports seven log levels, ordered from least to most verbose: - -| Level | Value | When to use | -|---------|-------|-------------| -| `fatal` | 1 | Unrecoverable errors that should always be visible | -| `error` | 2 | Recoverable errors or unexpected conditions | -| `warn` | 3 | Potential issues worth investigating | -| `info` | 4 | General operational information | -| `verbose` | 5 | Detailed information for troubleshooting | -| `debug` | 6 | Development-time debugging output | -| `trace` | 7 | Fine-grained tracing of execution flow | - -When you set `LOG_LEVEL=info`, you see `fatal`, `error`, `warn`, and `info` messages, but `verbose`, `debug`, and `trace` are hidden. - -Here is an example using all seven levels: - -#include_code log_all_levels /docs/examples/contracts/logging_example/src/main.nr rust - -## Simple log messages - -Each level has a function that accepts a plain string with no format arguments: - -#include_code log_simple /docs/examples/contracts/logging_example/src/main.nr rust - -## Log messages with format arguments - -Each level also has a `_format` variant that accepts a format string and an array of `Field` values. Use `{0}`, `{1}`, etc. to insert individual arguments by index, or `{}` to print the entire array: - -#include_code log_format_patterns /docs/examples/contracts/logging_example/src/main.nr rust - -:::note -Format arguments must be `Field` values. Use `.to_field()` to convert addresses and other types: - -#include_code log_address /docs/examples/contracts/logging_example/src/main.nr rust -::: - -## Viewing logs - -### In `aztec test` (Noir tests) - -To see contract logs, set `LOG_LEVEL` to include the `debug_log` module: - -```bash -LOG_LEVEL="error;trace:debug_log" aztec test -``` - -:::tip -Use different log levels strategically: add `info_log` calls for key state transitions you always want to see, and `debug_log` or `trace_log` calls for detailed inspection. -::: - -### In TypeScript tests (jest, vitest) - -TypeScript test environments do not enable contract logs by default. Set the `LOG_LEVEL` environment variable to include the `contract_log` module: - -```bash -# Show contract logs at debug level and above -LOG_LEVEL="info;debug:contract_log" yarn test - -# Show all contract log levels (most verbose) -LOG_LEVEL="error;trace:contract_log" yarn test -``` - -### With a local network - -Contract logs appear in the process that runs the PXE — your test process, not the network process. The network window only shows system-level infrastructure logs (archiver, world-state, etc.), which are generally not useful for contract debugging. - -When running TypeScript tests against a local network, set `LOG_LEVEL` on the **test command**: - -```bash -# Your test process sees contract logs -LOG_LEVEL="error;trace:contract_log" yarn test -``` - -You do not need to change the `LOG_LEVEL` on `aztec start --local-network` to see contract logs. - -## `LOG_LEVEL` syntax reference - -The `LOG_LEVEL` environment variable uses a semicolon-delimited format: - -``` -;:,;: -``` - -- **First segment (required)**: the default log level for all modules. A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level` — the parser always reads the segment before the first `;` as the default level. To filter only specific modules, start with `silent` (e.g. `LOG_LEVEL="silent;debug:simulator"`) -- **Remaining segments**: `level:module` pairs that override the default for specific modules -- Modules are comma-separated within a segment -- The `aztec:` prefix is automatically stripped from module names -- Module names support regex or prefix matching - -### Common configurations - -| Scenario | `LOG_LEVEL` value | -|----------|-------------------| -| Contract logs in `aztec test` (TXE) | `error;trace:debug_log` | -| Contract logs in TypeScript tests (PXE) | `error;trace:contract_log` | -| Contract debug+ logs with system info | `info;debug:contract_log` | -| Only contract warnings and errors | `warn;warn:contract_log` | -| Everything verbose | `verbose` | -| Debug a specific system module | `info;debug:sequencer` | -| Multiple module overrides | `warn;debug:sequencer,archiver;trace:contract_log` | - -## How contract logs are displayed - -### In `aztec test` (TXE) - -Contract logs appear under the `debug_log` module: - -``` -[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here -``` - -### In TypeScript tests (PXE) - -When running through the PXE, the output includes the contract name with an abbreviated address: - -``` -[07:40:29.937] INFO: contract_log::Counter(0x1234abcd) 164f9c87bca0cf8c Transfer completed -[07:40:29.947] DEBUG: contract_log::Counter(0x1234abcd) 164f9c87bca0cf8c Processing value: 0x2a -``` - -The hex value after the address (`164f9c87bca0cf8c`) is an internal request identifier. If the contract name cannot be resolved, you see `Unknown` in its place. - -## Logging in public functions - -Private and public functions handle logging differently: - -- **Private functions** execute locally in the PXE. You see logs immediately during simulation. -- **Public functions** execute on the sequencer. You see logs during `.simulate()` calls and after `.send().wait()` completes in test mode. - -The logging API is the same in public functions: - -#include_code log_public /docs/examples/contracts/logging_example/src/main.nr rust - -### Accessing logs programmatically - -In test mode (when not using real proofs), you can access public function debug logs on the `TxReceipt`: - -```typescript -import { applyStringFormatting } from '@aztec/foundation/log'; - -const { receipt } = await contract.methods.myPublicFunction(args).send({ - from: address, - fee: { paymentMethod }, - wait: { timeout: 600 }, -}); - -// Logs are automatically printed to your console. -// You can also access them programmatically: -if (receipt.debugLogs) { - for (const log of receipt.debugLogs) { - console.log(`[${log.level}] ${applyStringFormatting(log.message, log.fields)}`); - } -} -``` - -Each entry contains: - -- `contractAddress` - the contract that emitted the log -- `level` - the log level (`info`, `debug`, etc.) -- `message` - the unformatted message string -- `fields` - the raw `Field` values passed as arguments - -:::warning -`receipt.debugLogs` is only available in test mode (when not using real proofs). In production, debug log collection is disabled. -::: - -## Verify logging works - -Add a `debug_log` call to any contract function, then run with logging enabled: - -```bash -LOG_LEVEL="error;trace:debug_log" aztec test -``` - -You should see output like: - -``` -[07:40:29.947] DEBUG: txe:top_level_context:debug_log your message here -``` - -If no output appears, check the troubleshooting section below. - -## Troubleshooting - -| Problem | Solution | -|---------|----------| -| No contract logs appear in `aztec test` | Set `LOG_LEVEL` to include `debug_log`, e.g., `LOG_LEVEL="error;trace:debug_log" aztec test`. Also verify you are calling a log function inside the contract function being tested. | -| No contract logs in TypeScript tests | Set `LOG_LEVEL` to include `contract_log`, e.g., `LOG_LEVEL="error;trace:contract_log" yarn test`. | -| Import error on `dep::aztec::oracle::debug_log` | This path was removed. Update to `use aztec::oracle::logging::{debug_log, debug_log_format};`. | -| `receipt.debugLogs` is `undefined` | Debug logs are only collected in test mode (non-real-proofs). They are not available in production. | -| Too much noise in log output | Narrow the default level and use module filters, e.g., `LOG_LEVEL="error;debug:contract_log"`. | - -## Quick reference - -| Task | Code or command | -|------|-----------------| -| Import logging | `use aztec::oracle::logging::{debug_log, debug_log_format};` | -| Simple log | `debug_log("message");` | -| Log with values | `debug_log_format("val: {0}", [my_field]);` | -| Run Noir tests with logs | `LOG_LEVEL="error;trace:debug_log" aztec test` | -| JS tests with contract logs | `LOG_LEVEL="error;trace:contract_log" yarn test` | - -## Next steps - -- [Debugging Aztec Code](./debugging.md) for error codes, profiling, and common issues -- [Events and Logs](./framework-description/events_and_logs.md) for emitting events that offchain applications can consume -- [Testing Contracts](./testing_contracts.md) for writing and running contract tests From 14d2920e61f1745c881754edc258415b62a94201 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 21 Apr 2026 14:28:03 +0000 Subject: [PATCH 09/14] fix: adapt backport for v4-next file structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v4-next does not yet contain a version-v4.2.0/ snapshot — the closest snapshot is version-v4.2.0-aztecnr-rc.2/. The unversioned source still uses debugging.md (the rename to logging.md hasn't happened on this branch). Apply the equivalent intent of #22668 to the v4-next paths: - version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md: add LOG_LEVEL filter-format clarification + silent;debug:simulator example. The oracle::debug_log -> oracle::logging import-path fix is already present at this snapshot, so only the LOG_LEVEL block is changed. - version-v4.2.0-aztecnr-rc.2/docs/tutorials/contract_tutorials/counter_contract.md: fix the oracle::debug_log::debug_log_format bullet (the code sample above it already uses oracle::logging). aztec new counter is also already correct on this snapshot. - docs/docs-developers/docs/aztec-nr/debugging.md: add the same LOG_LEVEL filter-format clarification — equivalent of the unversioned logging.md change in the original PR. --- .../version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md | 5 +++++ .../docs/tutorials/contract_tutorials/counter_contract.md | 2 +- docs/docs-developers/docs/aztec-nr/debugging.md | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md index 7807187e4ffe..7dcc4f3b0fa0 100644 --- a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md +++ b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/aztec-nr/debugging.md @@ -69,6 +69,8 @@ LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_ - `level:module` - Sets level for a specific module - `level:module:submodule` - Sets level for a specific submodule +**The default-level filter must be the first segment.** A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level`, because the parser reads everything before the first `;` as the default level. To filter only specific modules, lead with a default level — use `silent` to suppress everything else. + ```bash # Default level only LOG_LEVEL="debug" @@ -78,6 +80,9 @@ LOG_LEVEL="info;debug:simulator;debug:execution" # Default level + specific submodule overrides LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_view_context" + +# Silence everything except one module +LOG_LEVEL="silent;debug:simulator" ``` ::: diff --git a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/tutorials/contract_tutorials/counter_contract.md b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/tutorials/contract_tutorials/counter_contract.md index b52beb725a64..bbfa2b3b547c 100644 --- a/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/developer_versioned_docs/version-v4.2.0-aztecnr-rc.2/docs/tutorials/contract_tutorials/counter_contract.md @@ -90,7 +90,7 @@ use balance_set::BalanceSet; - `messages::message_delivery::MessageDelivery` Imports `MessageDelivery` for specifying how note delivery should be handled (e.g., constrained onchain delivery). -- `oracle::debug_log::debug_log_format` +- `oracle::logging::debug_log_format` Imports a debug logging utility for printing formatted messages during contract execution. - `protocol::{address::AztecAddress, traits::ToField}` diff --git a/docs/docs-developers/docs/aztec-nr/debugging.md b/docs/docs-developers/docs/aztec-nr/debugging.md index 74bf4fb9c572..ca48d4cc7ab7 100644 --- a/docs/docs-developers/docs/aztec-nr/debugging.md +++ b/docs/docs-developers/docs/aztec-nr/debugging.md @@ -69,6 +69,8 @@ LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_ - `level:module` - Sets level for a specific module - `level:module:submodule` - Sets level for a specific submodule +**The default-level filter must be the first segment.** A bare `level:module` with no preceding default (e.g. `LOG_LEVEL="warn:simulator"`) is invalid and throws `Invalid log level`, because the parser reads everything before the first `;` as the default level. To filter only specific modules, lead with a default level — use `silent` to suppress everything else. + ```bash # Default level only LOG_LEVEL="debug" @@ -78,6 +80,9 @@ LOG_LEVEL="info;debug:simulator;debug:execution" # Default level + specific submodule overrides LOG_LEVEL="info;debug:simulator:client_execution_context;debug:simulator:client_view_context" + +# Silence everything except one module +LOG_LEVEL="silent;debug:simulator" ``` ::: From 47340d8c40f982ef9ec1848871cb867aa67567b9 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Tue, 21 Apr 2026 14:34:50 +0000 Subject: [PATCH 10/14] fix(docs): Noir LSP and binaries installed by the aztec installer (#22675) ## Summary Stop conflating `aztec` with `nargo` in the Noir LSP setup guide, and list every binary the installer actually provides. The docs led me down the wrong path while trying to get the LSP working when going through the counter contract tutorial. - `docs/aztec-nr/installation.md`: rewritten around `nargo` only. Drops the old `which aztec` guidance (which pointed users at `$HOME/.aztec/current/node_modules/.bin/aztec`, unusable as an LSP backend). Recommends leaving `Noir: Nargo Path` empty for auto-discovery; falls back to `which nargo`. Adds troubleshooting for `startFailed` and shadowed `nargo` on `PATH`. - `getting_started_on_local_network.md`: install list now matches the `aztec-install` output (adds `nargo`, `noir-profiler`, `bb`) and links to the Noir VSCode Extension guide. --- .../docs/aztec-nr/installation.md | 22 ++++++++++++++----- .../getting_started_on_local_network.md | 11 +++++++--- .../docs/aztec-nr/installation.md | 22 ++++++++++++++----- .../getting_started_on_local_network.md | 11 +++++++--- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/installation.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/installation.md index 05a8046b4345..7aa7a90b5a02 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/installation.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/installation.md @@ -5,14 +5,24 @@ tags: [local network, sandbox] description: Learn how to install and configure the Noir Language Server for a better development experience. --- -Install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) to get syntax highlighting, syntax error detection and go-to definitions for your Aztec contracts. +Install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) to get syntax highlighting, syntax error detection, and go-to definitions for your Aztec contracts. -Once the extension is installed, check your nargo binary by hovering over Nargo in the status bar on the bottom right of the application window. Click to choose the path to `aztec` (or regular nargo, if you have that installed). - -You can print the path of your `aztec` executable by running: +The extension drives its language server with `nargo`. The Aztec installer installs its own version of `nargo` and adds that directory to your `PATH`, so in most cases you do not need to configure anything else. Verify the binary is on your `PATH`: ```bash -which aztec +which nargo +# expected: $HOME/.aztec/ ``` -To specify a custom nargo executable, go to the VSCode settings and search for "noir", or click extension settings on the `noir-lang` LSP plugin. Update the `Noir: Nargo Path` field to point to your desired `aztec` executable. +If you have not installed the Aztec toolchain yet, follow [Getting Started on Local Network](../../getting_started_on_local_network.md) first. + +## Configure the extension + +Leave the extension's `Noir: Nargo Path` setting empty so it auto-discovers `nargo` from your `PATH`. To confirm, hover over **Nargo** in the VSCode status bar in the bottom right corner — it should show the path under the result from `which nargo`. + +If auto-discovery fails, set `Noir: Nargo Path` to the absolute path printed by `which nargo`, then reload the window. + +## Troubleshooting + +- **LSP reports `startFailed` after setting a custom path**: clear `Noir: Nargo Path`, reload the window, and let auto-discovery take over. +- **`which nargo` points outside `$HOME/.aztec/current/bin`**: another `nargo` earlier on your `PATH` is shadowing the Aztec-provided one. Either remove it or set `Noir: Nargo Path` explicitly to the Aztec-provided `nargo`. diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/getting_started_on_local_network.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/getting_started_on_local_network.md index a9715151ab20..303d8cf0a766 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/getting_started_on_local_network.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/getting_started_on_local_network.md @@ -37,11 +37,16 @@ Run: VERSION=4.1.0-rc.2 bash -i <(curl -sL https://install.aztec.network/4.1.0-rc.2) ``` -This will install the following tools: +This will install the following tools and add them to your `PATH`: -- **aztec** - compiles and tests aztec contracts and launches various infrastructure subsystems (full local network, sequencer, prover, pxe, etc) and provides utility commands to interact with the network +- **nargo** - the Noir programming language compiler and simulator +- **noir-profiler** - a profiler for analyzing and visualizing Noir programs +- **bb** - the Barretenberg proving backend +- **aztec** - compiles and tests Aztec contracts and launches various infrastructure subsystems (full local network, sequencer, prover, PXE, etc.) and provides utility commands to interact with the network - **aztec-up** - a version manager for the Aztec toolchain. Use `aztec-up install ` to install a new version, `aztec-up use ` to switch between installed versions, or `aztec-up list` to see installed versions. -- **aztec-wallet** - a tool for interacting with the aztec network +- **aztec-wallet** - a tool for interacting with the Aztec network + +For syntax highlighting and LSP support while editing contracts, see the [Noir VSCode Extension guide](./docs/aztec-nr/installation.md). ### Start the local network diff --git a/docs/docs-developers/docs/aztec-nr/installation.md b/docs/docs-developers/docs/aztec-nr/installation.md index 05a8046b4345..7aa7a90b5a02 100644 --- a/docs/docs-developers/docs/aztec-nr/installation.md +++ b/docs/docs-developers/docs/aztec-nr/installation.md @@ -5,14 +5,24 @@ tags: [local network, sandbox] description: Learn how to install and configure the Noir Language Server for a better development experience. --- -Install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) to get syntax highlighting, syntax error detection and go-to definitions for your Aztec contracts. +Install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) to get syntax highlighting, syntax error detection, and go-to definitions for your Aztec contracts. -Once the extension is installed, check your nargo binary by hovering over Nargo in the status bar on the bottom right of the application window. Click to choose the path to `aztec` (or regular nargo, if you have that installed). - -You can print the path of your `aztec` executable by running: +The extension drives its language server with `nargo`. The Aztec installer installs its own version of `nargo` and adds that directory to your `PATH`, so in most cases you do not need to configure anything else. Verify the binary is on your `PATH`: ```bash -which aztec +which nargo +# expected: $HOME/.aztec/ ``` -To specify a custom nargo executable, go to the VSCode settings and search for "noir", or click extension settings on the `noir-lang` LSP plugin. Update the `Noir: Nargo Path` field to point to your desired `aztec` executable. +If you have not installed the Aztec toolchain yet, follow [Getting Started on Local Network](../../getting_started_on_local_network.md) first. + +## Configure the extension + +Leave the extension's `Noir: Nargo Path` setting empty so it auto-discovers `nargo` from your `PATH`. To confirm, hover over **Nargo** in the VSCode status bar in the bottom right corner — it should show the path under the result from `which nargo`. + +If auto-discovery fails, set `Noir: Nargo Path` to the absolute path printed by `which nargo`, then reload the window. + +## Troubleshooting + +- **LSP reports `startFailed` after setting a custom path**: clear `Noir: Nargo Path`, reload the window, and let auto-discovery take over. +- **`which nargo` points outside `$HOME/.aztec/current/bin`**: another `nargo` earlier on your `PATH` is shadowing the Aztec-provided one. Either remove it or set `Noir: Nargo Path` explicitly to the Aztec-provided `nargo`. diff --git a/docs/docs-developers/getting_started_on_local_network.md b/docs/docs-developers/getting_started_on_local_network.md index d72d34dd0360..b390b3f39838 100644 --- a/docs/docs-developers/getting_started_on_local_network.md +++ b/docs/docs-developers/getting_started_on_local_network.md @@ -42,11 +42,16 @@ Run: VERSION=#include_version_without_prefix bash -i <(curl -sL https://install.aztec.network/#include_version_without_prefix) ``` -This will install the following tools: +This will install the following tools and add them to your `PATH`: -- **aztec** - compiles and tests aztec contracts and launches various infrastructure subsystems (full local network, sequencer, prover, pxe, etc) and provides utility commands to interact with the network +- **nargo** - the Noir programming language compiler and simulator +- **noir-profiler** - a profiler for analyzing and visualizing Noir programs +- **bb** - the Barretenberg proving backend +- **aztec** - compiles and tests Aztec contracts and launches various infrastructure subsystems (full local network, sequencer, prover, PXE, etc.) and provides utility commands to interact with the network - **aztec-up** - a version manager for the Aztec toolchain. Use `aztec-up install ` to install a new version, `aztec-up use ` to switch between installed versions, or `aztec-up list` to see installed versions. -- **aztec-wallet** - a tool for interacting with the aztec network +- **aztec-wallet** - a tool for interacting with the Aztec network + +For syntax highlighting and LSP support while editing contracts, see the [Noir VSCode Extension guide](./docs/aztec-nr/installation.md). ### Start the local network From 8dd41bde49572e89b45415b04e4e7c2e0a11fe84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 21 Apr 2026 16:09:50 -0300 Subject: [PATCH 11/14] cherry-pick: docs: link to apiref, not gh, remove stale include code markers (#22649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked d84d562134 as-is. Conflict markers intact in: - docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md - docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md - docs/examples/ts/recursive_verification/index.ts Modify/delete conflicts (files removed on v4-next but modified upstream) left as the upstream modified version in tree — will be resolved in the next commit per .claude/claudebox/backport.md. --- boxes/boxes/vanilla/contracts/src/main.nr | 8 - .../advanced/protocol_oracles.md | 12 + .../docs/aztec-nr/testing_contracts.md | 4 + .../advanced/protocol_oracles.md | 13 +- .../docs/aztec-nr/testing_contracts.md | 2 +- .../Nargo.toml | 2 - .../src/main.nr | 14 - .../solidity/example_swap/ExampleERC20.sol | 15 + .../example_swap/ExampleTokenPortal.sol | 108 ++ .../ts/recursive_verification/index.ts | 10 +- .../webapp-tutorial/contracts/src/main.nr | 182 +++ docs/examples/webapp-tutorial/src/App.tsx | 131 ++ .../src/components/ErrorBoundary.tsx | 47 + .../src/components/GameBoard.tsx | 151 ++ .../src/components/GameLobby.tsx | 154 ++ .../src/components/GameStatus.tsx | 36 + .../src/components/TxStatus.tsx | 34 + .../src/components/WalletConnect.tsx | 198 +++ docs/examples/webapp-tutorial/src/config.ts | 33 + docs/examples/webapp-tutorial/src/contract.ts | 120 ++ .../webapp-tutorial/src/embedded-wallet.ts | 144 ++ docs/examples/webapp-tutorial/src/fees.ts | 47 + .../webapp-tutorial/src/game-constants.ts | 5 + .../webapp-tutorial/src/wallet-connection.ts | 112 ++ .../test-extension/src/account-utils.ts | 60 + .../test-extension/src/aztec-imports.ts | 125 ++ .../test-extension/src/background.ts | 1236 +++++++++++++++++ .../test-extension/src/config.ts | 96 ++ .../test-extension/src/offscreen/offscreen.ts | 771 ++++++++++ .../src/popup/AccountSwitcher.tsx | 51 + .../test-extension/src/popup/ApprovalView.tsx | 509 +++++++ .../src/popup/CreateAccountView.tsx | 49 + .../test-extension/src/popup/Header.tsx | 104 ++ .../test-extension/src/popup/LockScreen.tsx | 60 + .../test-extension/src/popup/SettingsPage.tsx | 154 ++ .../test-extension/src/popup/SetupScreen.tsx | 130 ++ .../test-extension/src/popup/helpers.ts | 100 ++ .../test-extension/src/popup/popup.tsx | 467 +++++++ .../test-extension/src/popup/types.ts | 18 + .../test-extension/src/shared-types.ts | 97 ++ .../test-extension/src/utils.ts | 44 + .../test-extension/src/wallet/storage.ts | 333 +++++ .../test-extension/src/wallet/wallet-impl.ts | 153 ++ l1-contracts/test/portals/TokenPortal.sol | 2 - l1-contracts/test/portals/UniswapPortal.sol | 8 - .../aztec-nr/aztec/src/authwit/account.nr | 1 - .../aztec/src/authwit/entrypoint/app.nr | 1 - .../aztec-nr/aztec/src/oracle/mod.nr | 4 - .../aztec-nr/aztec/src/state_vars/map.nr | 1 - .../aztec/src/state_vars/private_mutable.nr | 2 - .../aztec-nr/uint-note/src/uint_note.nr | 2 - .../schnorr_account_contract/src/main.nr | 2 - .../contracts/app/auth_contract/src/main.nr | 2 - .../app/crowdfunding_contract/src/main.nr | 2 - .../contracts/app/escrow_contract/src/main.nr | 4 - .../app/lending_contract/src/asset.nr | 2 - .../app/lending_contract/src/main.nr | 4 - .../contracts/app/nft_contract/src/main.nr | 9 - .../app/private_token_contract/src/main.nr | 2 - .../app/private_voting_contract/src/main.nr | 8 - .../app/token_blacklist_contract/src/main.nr | 2 - .../app/token_bridge_contract/src/main.nr | 2 - .../contracts/app/token_contract/src/main.nr | 15 - .../app/uniswap_contract/src/main.nr | 6 - .../app/uniswap_contract/src/util.nr | 4 - .../contracts/fees/fpc_contract/src/main.nr | 4 - .../test/counter/counter_contract/src/main.nr | 12 - .../src/filter.nr | 2 - .../crates/serde/src/serialization.nr | 2 - yarn-project/cli-wallet/test/flows/basic.sh | 2 - .../test/flows/create_account_pay_native.sh | 4 - .../e2e_local_network_example.test.ts | 7 - .../e2e_token_bridge_tutorial_test.test.ts | 28 - .../uniswap_trade_on_l1_from_l2.test.ts | 2 - .../end-to-end/src/guides/up_quick_start.sh | 2 - .../src/shared/gas_portal_test_harness.ts | 1 - .../end-to-end/src/shared/uniswap_l1_l2.ts | 4 - .../contract_function_simulator.ts | 2 - 78 files changed, 6100 insertions(+), 196 deletions(-) create mode 100644 docs/examples/solidity/example_swap/ExampleERC20.sol create mode 100644 docs/examples/solidity/example_swap/ExampleTokenPortal.sol create mode 100644 docs/examples/webapp-tutorial/contracts/src/main.nr create mode 100644 docs/examples/webapp-tutorial/src/App.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/GameBoard.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/GameLobby.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/GameStatus.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/TxStatus.tsx create mode 100644 docs/examples/webapp-tutorial/src/components/WalletConnect.tsx create mode 100644 docs/examples/webapp-tutorial/src/config.ts create mode 100644 docs/examples/webapp-tutorial/src/contract.ts create mode 100644 docs/examples/webapp-tutorial/src/embedded-wallet.ts create mode 100644 docs/examples/webapp-tutorial/src/fees.ts create mode 100644 docs/examples/webapp-tutorial/src/game-constants.ts create mode 100644 docs/examples/webapp-tutorial/src/wallet-connection.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/account-utils.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/background.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/config.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx create mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/types.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/shared-types.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/utils.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts create mode 100644 docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts diff --git a/boxes/boxes/vanilla/contracts/src/main.nr b/boxes/boxes/vanilla/contracts/src/main.nr index 5062ff09c892..5112d2109516 100644 --- a/boxes/boxes/vanilla/contracts/src/main.nr +++ b/boxes/boxes/vanilla/contracts/src/main.nr @@ -3,7 +3,6 @@ use aztec::macros::aztec; #[aztec] pub contract PrivateVoting { - // docs:start:imports use aztec::macros::{functions::{external, initializer, only_self, view}, storage::storage}; use aztec::protocol::{address::AztecAddress, traits::{Deserialize, Serialize, ToField}}; use aztec::state_vars::{Map, Owned, PublicImmutable, PublicMutable, SingleUseClaim}; @@ -25,8 +24,6 @@ pub contract PrivateVoting { } } - // docs:end:imports - // docs:start:storage_struct #[storage] struct Storage { // admin can start and end elections @@ -40,15 +37,12 @@ pub contract PrivateVoting { // election => voter => single use claim that ensures voter can at most vote once per election vote_claims: Map, Context>, Context>, } - // docs:end:storage_struct - // docs:start:constructor #[external("public")] #[initializer] fn constructor(admin: AztecAddress) { self.storage.admin.write(admin); } - // docs:end:constructor #[external("private")] fn cast_vote(election_id: ElectionId, candidate: Field) { @@ -56,7 +50,6 @@ pub contract PrivateVoting { self.enqueue_self.add_to_tally_public(election_id, candidate); } - // docs:start:nested_map_access #[external("public")] #[only_self] fn add_to_tally_public(election_id: ElectionId, candidate: Field) { @@ -65,7 +58,6 @@ pub contract PrivateVoting { let new_tally = self.storage.tally.at(election_id).at(candidate).read() + 1; self.storage.tally.at(election_id).at(candidate).write(new_tally); } - // docs:end:nested_map_access #[external("public")] fn start_vote(election_id: ElectionId) { diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md index b93ffc2c2ce9..9a33852f51f2 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md @@ -21,6 +21,7 @@ If we fetch the notes using an oracle call, we can keep the function signature i Oracles introduce **non-determinism** into a circuit, and thus are `unconstrained`. It is important that any information that is injected into a circuit through an oracle is later constrained for correctness. Otherwise, the circuit will be **under-constrained** and potentially insecure! +<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md `Aztec.nr` has a module dedicated to its oracles. If you are interested, you can view them by following the link below: ```rust title="oracles-module" showLineNumbers /// Oracles module @@ -35,6 +36,17 @@ Oracles introduce **non-determinism** into a circuit, and thus are `unconstraine - [`get_l1_to_l2_membership_witness`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. - [`notes`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/notes.nr) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. - [`logs`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/logs.nr) - Provides functions to log encrypted and unencrypted data. +======= +`Aztec.nr` has a [module dedicated to its oracles](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/index.html) where you can browse the full list. + +## Inbuilt oracles + +- [`debug_log`](pathname:///aztec-nr-api/mainnet/noir_aztec/protocol/logging/fn.debug_log) - Provides debug functions that can be used to log information to the console. Read more about debugging [here](../../debugging.md). +- [`auth_witness`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/auth_witness/index.html) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. +- [`get_l1_to_l2_membership_witness`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/get_l1_to_l2_membership_witness/index.html) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. +- [`notes`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/notes/index.html) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. +- [`logs`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/logs/index.html) - Provides functions to log encrypted and unencrypted data. +>>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-nr/framework-description/advanced/protocol_oracles.md Find a full list [on GitHub](https://github.com/AztecProtocol/aztec-packages/tree/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle). diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md index 48dbdcb6c7c1..1e8bd3f4e74c 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md @@ -67,7 +67,11 @@ unconstrained fn test_basic_flow() { - Tests run in parallel by default - Use `unconstrained` functions for faster execution +<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md - See all `TestEnvironment` methods [here](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr) +======= +- See all `TestEnvironment` methods [here](pathname:///aztec-nr-api/mainnet/noir_aztec/test/helpers/test_environment/struct.TestEnvironment) +>>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-nr/testing_contracts.md ::: diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md index 70cdbaeb1ada..bed2bbad8e6f 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md @@ -21,16 +21,15 @@ If we fetch the notes using an oracle call, we can keep the function signature i Oracles introduce **non-determinism** into a circuit, and thus are `unconstrained`. It is important that any information that is injected into a circuit through an oracle is later constrained for correctness. Otherwise, the circuit will be **under-constrained** and potentially insecure! -`Aztec.nr` has a module dedicated to its oracles. If you are interested, you can view them by following the link below: -#include_code oracles-module /noir-projects/aztec-nr/aztec/src/oracle/mod.nr rust +`Aztec.nr` has a [module dedicated to its oracles](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/index.html) where you can browse the full list. ## Inbuilt oracles -- [`debug_log`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-protocol-circuits/crates/types/src/debug_log.nr) - Provides debug functions that can be used to log information to the console. Read more about debugging [here](../../debugging.md). -- [`auth_witness`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. -- [`get_l1_to_l2_membership_witness`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. -- [`notes`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle/notes.nr) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. -- [`logs`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle/logs.nr) - Provides functions to log encrypted and unencrypted data. +- [`debug_log`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/protocol/logging/fn.debug_log) - Provides debug functions that can be used to log information to the console. Read more about debugging [here](../../debugging.md). +- [`auth_witness`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/auth_witness/index.html) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. +- [`get_l1_to_l2_membership_witness`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/get_l1_to_l2_membership_witness/index.html) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. +- [`notes`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/notes/index.html) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. +- [`logs`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/oracle/logs/index.html) - Provides functions to log encrypted and unencrypted data. Find a full list [on GitHub](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/oracle). diff --git a/docs/docs-developers/docs/aztec-nr/testing_contracts.md b/docs/docs-developers/docs/aztec-nr/testing_contracts.md index 801b180fe240..cfde168a0415 100644 --- a/docs/docs-developers/docs/aztec-nr/testing_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/testing_contracts.md @@ -83,7 +83,7 @@ unconstrained fn test_basic_flow() { - Tests run in parallel by default - Use `unconstrained` functions for faster execution -- See all `TestEnvironment` methods [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr) +- See all `TestEnvironment` methods [here](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/test/helpers/test_environment/struct.TestEnvironment) ::: diff --git a/docs/examples/contracts/recursive_verification_contract/Nargo.toml b/docs/examples/contracts/recursive_verification_contract/Nargo.toml index 1542b5e2b2ca..0b76eb518025 100644 --- a/docs/examples/contracts/recursive_verification_contract/Nargo.toml +++ b/docs/examples/contracts/recursive_verification_contract/Nargo.toml @@ -1,4 +1,3 @@ -# docs:start:nargo_toml [package] name = "recursive_verification_contract" type = "contract" @@ -8,4 +7,3 @@ compiler_version = ">=0.25.0" [dependencies] aztec = { path = "../../../../noir-projects/aztec-nr/aztec" } bb_proof_verification = { path = "../../../../barretenberg/noir/bb_proof_verification" } -# docs:end:nargo_toml diff --git a/docs/examples/contracts/recursive_verification_contract/src/main.nr b/docs/examples/contracts/recursive_verification_contract/src/main.nr index bc1b00f16f09..ea47f2fccbd1 100644 --- a/docs/examples/contracts/recursive_verification_contract/src/main.nr +++ b/docs/examples/contracts/recursive_verification_contract/src/main.nr @@ -1,11 +1,8 @@ // docs:start:full_contract -// docs:start:contract use aztec::macros::aztec; #[aztec] pub contract ValueNotEqual { - // docs:end:contract - // docs:start:imports use aztec::{ macros::{functions::{external, initializer, only_self, view}, storage::storage}, oracle::logging::debug_log_format, @@ -13,26 +10,20 @@ pub contract ValueNotEqual { state_vars::{Map, PublicImmutable, PublicMutable}, }; use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; - // docs:end:imports - // docs:start:storage #[storage] struct Storage { counters: Map, Context>, vk_hash: PublicImmutable, } - // docs:end:storage - // docs:start:constructor #[initializer] #[external("public")] fn constructor(headstart: Field, owner: AztecAddress, vk_hash: Field) { self.storage.counters.at(owner).write(headstart); self.storage.vk_hash.initialize(vk_hash); } - // docs:end:constructor - // docs:start:increment #[external("private")] fn increment( owner: AztecAddress, @@ -56,23 +47,18 @@ pub contract ValueNotEqual { // Enqueue a public function call to update state self.enqueue_self._increment_public(owner); } - // docs:end:increment - // docs:start:increment_public #[only_self] #[external("public")] fn _increment_public(owner: AztecAddress) { let current = self.storage.counters.at(owner).read(); self.storage.counters.at(owner).write(current + 1); } - // docs:end:increment_public - // docs:start:get_counter #[view] #[external("public")] fn get_counter(owner: AztecAddress) -> Field { self.storage.counters.at(owner).read() } - // docs:end:get_counter } // docs:end:full_contract diff --git a/docs/examples/solidity/example_swap/ExampleERC20.sol b/docs/examples/solidity/example_swap/ExampleERC20.sol new file mode 100644 index 000000000000..6cbaa356f515 --- /dev/null +++ b/docs/examples/solidity/example_swap/ExampleERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +/// @title ExampleERC20 +/// @notice Minimal ERC20 with public mint for testing L1<>L2 swap flows. +contract ExampleERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + /// @notice Anyone can mint tokens (test only!) + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/docs/examples/solidity/example_swap/ExampleTokenPortal.sol b/docs/examples/solidity/example_swap/ExampleTokenPortal.sol new file mode 100644 index 000000000000..5e728b795604 --- /dev/null +++ b/docs/examples/solidity/example_swap/ExampleTokenPortal.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +// docs:start:example_token_portal +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; + +/// @title ExampleTokenPortal +/// @notice Example token portal for tutorial. +contract ExampleTokenPortal { + using SafeERC20 for IERC20; + + IRegistry public registry; + IERC20 public underlying; + bytes32 public l2Bridge; + + IRollup public rollup; + IOutbox public outbox; + IInbox public inbox; + uint256 public rollupVersion; + + /// @dev No access control for simplicity. A production contract should restrict this to the deployer/owner. + function initialize( + address _registry, + address _underlying, + bytes32 _l2Bridge + ) external { + registry = IRegistry(_registry); + underlying = IERC20(_underlying); + l2Bridge = _l2Bridge; + + rollup = IRollup(address(registry.getCanonicalRollup())); + outbox = rollup.getOutbox(); + inbox = rollup.getInbox(); + rollupVersion = rollup.getVersion(); + } + // docs:end:example_token_portal + + // docs:start:deposit_to_aztec_public + /// @notice Deposit tokens and send L1->L2 message for public minting on Aztec + function depositToAztecPublic( + bytes32 _to, + uint256 _amount, + bytes32 _secretHash + ) external returns (bytes32, uint256) { + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + + bytes32 contentHash = Hash.sha256ToField( + abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, _amount) + ); + + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + return inbox.sendL2Message(actor, contentHash, _secretHash); + } + // docs:end:deposit_to_aztec_public + + /// @notice Deposit tokens and send L1->L2 message for private minting on Aztec + function depositToAztecPrivate( + uint256 _amount, + bytes32 _secretHash + ) external returns (bytes32, uint256) { + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); + + bytes32 contentHash = Hash.sha256ToField( + abi.encodeWithSignature("mint_to_private(uint256)", _amount) + ); + + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + return inbox.sendL2Message(actor, contentHash, _secretHash); + } + + // docs:start:withdraw + /// @notice Withdraw tokens after consuming an L2->L1 message. + function withdraw( + address _recipient, + uint256 _amount, + Epoch _epoch, + uint256 _leafIndex, + bytes32[] calldata _path + ) external { + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2Bridge, rollupVersion), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: Hash.sha256ToField( + abi.encodeWithSignature( + "withdraw(address,uint256,address)", + _recipient, + _amount, + msg.sender + ) + ) + }); + + outbox.consume(message, _epoch, _leafIndex, _path); + + underlying.safeTransfer(_recipient, _amount); + } + // docs:end:withdraw +} diff --git a/docs/examples/ts/recursive_verification/index.ts b/docs/examples/ts/recursive_verification/index.ts index 330ae056a540..15c19c66675c 100644 --- a/docs/examples/ts/recursive_verification/index.ts +++ b/docs/examples/ts/recursive_verification/index.ts @@ -1,5 +1,4 @@ // docs:start:run_recursion -// docs:start:imports import { SponsoredFeePaymentMethod } from "@aztec/aztec.js/fee"; import type { FieldLike } from "@aztec/aztec.js/abi"; import { getSponsoredFPCInstance } from "./scripts/sponsored_fpc.js"; @@ -9,10 +8,12 @@ import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { NO_FROM } from "@aztec/aztec.js/account"; import { Fr } from "@aztec/aztec.js/fields"; import fs from "node:fs"; +<<<<<<< HEAD import assert from "node:assert"; // docs:end:imports +======= +>>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)) -// docs:start:sample_data if (!fs.existsSync("data.json")) { console.error( "data.json not found. Run 'yarn data' first to generate proof data.", @@ -20,11 +21,9 @@ if (!fs.existsSync("data.json")) { process.exit(1); } const data = JSON.parse(fs.readFileSync("data.json", "utf-8")); -// docs:end:sample_data export const NODE_URL = process.env.AZTEC_NODE_URL ?? "http://localhost:8080"; -// docs:start:setup_wallet // Setup sponsored fee payment - the FPC pays transaction fees for us const sponsoredFPC = await getSponsoredFPCInstance(); const sponsoredPaymentMethod = new SponsoredFeePaymentMethod( @@ -47,9 +46,7 @@ export const setupWallet = async (): Promise => { throw error; } }; -// docs:end:setup_wallet -// docs:start:main async function main() { // Step 1: Setup wallet and create account // Accounts in Aztec are smart contracts (account abstraction) @@ -120,7 +117,6 @@ async function main() { assert(counterValue === 11n, "Counter should be 11 after verification"); } -// docs:end:main main().catch((error) => { console.error(error); diff --git a/docs/examples/webapp-tutorial/contracts/src/main.nr b/docs/examples/webapp-tutorial/contracts/src/main.nr new file mode 100644 index 000000000000..872daafd4370 --- /dev/null +++ b/docs/examples/webapp-tutorial/contracts/src/main.nr @@ -0,0 +1,182 @@ +// Pod Racing Game Contract +// +// A two-player competitive racing game where players allocate points across 5 tracks +// over multiple rounds. The game flow: +// 1. Player 1 creates a game with a time limit +// 2. Player 2 joins the game +// 3. Both players play rounds privately (allocating points across tracks) +// 4. After all rounds, players reveal their total scores per track +// 5. Winner is determined by who won more tracks (best of 5) +// +// Key mechanics: +// - Each round, players distribute up to 9 points across 5 tracks +// - Round choices are private until the finish phase +// - The player with higher total points on a track wins that track +// - The player who wins 3+ tracks wins the game + +mod game_round_note; +mod race; + +use aztec::macros::aztec; + +#[aztec] +pub contract PodRacing { + use aztec::{ + macros::{functions::{external, initializer, only_self}, storage::storage}, + messages::message_delivery::MessageDelivery, + note::note_getter_options::NoteGetterOptions, + }; + use aztec::protocol::address::AztecAddress; + use aztec::state_vars::{Map, Owned, PrivateSet, PublicMutable}; + + use crate::{game_round_note::GameRoundNote, race::Race}; + + global TOTAL_ROUNDS: u8 = 3; + global GAME_LENGTH: u32 = 2; // Set to 2 for demo purposes + + // docs:start:storage + #[storage] + struct Storage { + admin: PublicMutable, + races: Map, Context>, + progress: Map, Context>, Context>, + win_history: Map, Context>, + } + // docs:end:storage + + #[external("public")] + #[initializer] + fn constructor(admin: AztecAddress) { + self.storage.admin.write(admin); + } + + // docs:start:create-game + #[external("public")] + fn create_game(game_id: Field) { + assert(self.storage.races.at(game_id).read().player1.eq(AztecAddress::zero())); + let game = Race::new( + self.msg_sender(), + TOTAL_ROUNDS, + self.context.block_number() + GAME_LENGTH, + ); + self.storage.races.at(game_id).write(game); + } + // docs:end:create-game + + // docs:start:join-game + #[external("public")] + fn join_game(game_id: Field) { + let maybe_existing_game = self.storage.races.at(game_id).read(); + let joined_game = maybe_existing_game.join(self.msg_sender()); + self.storage.races.at(game_id).write(joined_game); + } + // docs:end:join-game + + // docs:start:play-round + /// Allocates points across 5 tracks for a round. + /// This is a PRIVATE function - the allocation remains hidden from the opponent. + #[external("private")] + fn play_round( + game_id: Field, + round: u8, + track1: u8, + track2: u8, + track3: u8, + track4: u8, + track5: u8, + ) { + assert(track1 + track2 + track3 + track4 + track5 < 10); + + let player = self.msg_sender(); + + self + .storage + .progress + .at(game_id) + .at(player) + .insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player)) + .deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + + self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round( + player, + game_id, + round, + )); + } + // docs:end:play-round + + #[external("public")] + #[only_self] + fn validate_and_play_round(player: AztecAddress, game_id: Field, round: u8) { + let game_in_progress = self.storage.races.at(game_id).read(); + self.storage.races.at(game_id).write(game_in_progress.increment_player_round(player, round)); + } + + // docs:start:finish-game + /// Reveals a player's total scores per track. + /// Reads private round notes and publishes aggregated totals. + #[external("private")] + fn finish_game(game_id: Field) { + let player = self.msg_sender(); + let totals = + self.storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new()); + + let mut total_track1: u64 = 0; + let mut total_track2: u64 = 0; + let mut total_track3: u64 = 0; + let mut total_track4: u64 = 0; + let mut total_track5: u64 = 0; + + for i in 0..TOTAL_ROUNDS { + total_track1 += totals.get(i as u32).note.track1 as u64; + total_track2 += totals.get(i as u32).note.track2 as u64; + total_track3 += totals.get(i as u32).note.track3 as u64; + total_track4 += totals.get(i as u32).note.track4 as u64; + total_track5 += totals.get(i as u32).note.track5 as u64; + } + + self.enqueue(PodRacing::at(self.context.this_address()).validate_finish_game_and_reveal( + player, + game_id, + total_track1, + total_track2, + total_track3, + total_track4, + total_track5, + )); + } + // docs:end:finish-game + + #[external("public")] + #[only_self] + fn validate_finish_game_and_reveal( + player: AztecAddress, + game_id: Field, + total_track1: u64, + total_track2: u64, + total_track3: u64, + total_track4: u64, + total_track5: u64, + ) { + let game_in_progress = self.storage.races.at(game_id).read(); + self.storage.races.at(game_id).write(game_in_progress.set_player_scores( + player, + total_track1, + total_track2, + total_track3, + total_track4, + total_track5, + )); + } + + // docs:start:finalize-game + /// Determines the winner after both players have revealed and the game has expired. + #[external("public")] + fn finalize_game(game_id: Field) { + let game_in_progress = self.storage.races.at(game_id).read(); + let winner = game_in_progress.calculate_winner(self.context.block_number()); + let previous_wins = self.storage.win_history.at(winner).read(); + self.storage.win_history.at(winner).write(previous_wins + 1); + } + // docs:end:finalize-game +} diff --git a/docs/examples/webapp-tutorial/src/App.tsx b/docs/examples/webapp-tutorial/src/App.tsx new file mode 100644 index 000000000000..373b4ba55a18 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/App.tsx @@ -0,0 +1,131 @@ +// docs:start:app-imports +import React, { useState } from 'react'; +import type { Wallet } from '@aztec/aztec.js/wallet'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { NetworkType } from './config'; +import type { PodRacingContract } from './artifacts/PodRacing'; +import { NetworkPicker } from './components/NetworkPicker'; +import { WalletConnect } from './components/WalletConnect'; +import { AccountInfo } from './components/AccountInfo'; +import { GameLobby } from './components/GameLobby'; +import { GameBoard } from './components/GameBoard'; +import { GameStatus } from './components/GameStatus'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { LogProvider, TransactionLog } from './components/TransactionLog'; +import { TwoPlayerLocal } from './components/TwoPlayerLocal'; +import { EmbeddedWallet } from './embedded-wallet'; +// docs:end:app-imports + +// docs:start:app-state +type AppPhase = 'connect' | 'lobby' | 'playing'; + +function App() { + const [network, setNetwork] = useState('local'); + const [wallet, setWallet] = useState(null); + const [account, setAccount] = useState(null); + const [phase, setPhase] = useState('connect'); + const [contract, setContract] = useState(null); + const [gameId, setGameId] = useState(BigInt(0)); + const [currentRound, setCurrentRound] = useState(1); +// docs:end:app-state + + // docs:start:app-handlers + async function handleWalletConnected(w: Wallet | EmbeddedWallet) { + setWallet(w); + if (w instanceof EmbeddedWallet) { + setAccount(w.getConnectedAccount()); + setPhase('lobby'); + } else { + // Extension wallet — getAccounts returns the active account(s) + try { + const accounts = await w.getAccounts(); + console.log('Accounts received:', accounts); + if (accounts && accounts.length > 0) { + const addr = accounts[0].item; + console.log('Setting account:', addr); + setAccount(addr); + setPhase('lobby'); + } else { + alert('Please create an account in the wallet extension first, then refresh the page.'); + } + } catch (err: unknown) { + console.error('Error getting accounts:', err); + alert(`Error connecting to wallet: ${err}`); + } + } + } + + function handleGameJoined(c: PodRacingContract, gId: bigint) { + setContract(c); + setGameId(gId); + setCurrentRound(1); + setPhase('playing'); + } + + function handleRoundPlayed() { + setCurrentRound((r) => r + 1); + } + // docs:end:app-handlers + + // docs:start:app-render + return ( + + +
+
+

Pod Racing on Aztec

+ + {network === 'remote' && account && } +
+ +
+ {/* Local network: Two-player split-screen mode */} + {network === 'local' && } + + {/* Remote: Single-player mode with wallet extension */} + {network === 'remote' && phase === 'connect' && ( + + )} + + {network === 'remote' && phase === 'lobby' && wallet && account && ( + + )} + + {network === 'remote' && phase === 'playing' && wallet && account && contract && ( +
+ + +
+ )} + + +
+
+
+
+ ); + // docs:end:app-render +} + +export { App }; diff --git a/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx b/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx new file mode 100644 index 000000000000..57deabff998d --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +/** + * Catches unhandled errors during rendering and shows a fallback UI + * instead of crashing the entire application with a blank screen. + */ +export class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

The application encountered an unexpected error.

+ + {this.state.error && ( +
{this.state.error.message}
+ )} +
+ ); + } + + return this.props.children; + } +} diff --git a/docs/examples/webapp-tutorial/src/components/GameBoard.tsx b/docs/examples/webapp-tutorial/src/components/GameBoard.tsx new file mode 100644 index 000000000000..671d05d0cc6b --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/GameBoard.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { PodRacingContract } from '../artifacts/PodRacing'; +import { playRound, finishGame, finalizeGame } from '../contract'; +import { TRACK_NAMES, MAX_POINTS_PER_ROUND, TOTAL_ROUNDS } from '../game-constants'; +import { useTransactionLog } from './TransactionLog'; + +interface GameBoardProps { + contract: PodRacingContract; + account: AztecAddress; + gameId: bigint; + currentRound: number; + onRoundPlayed: () => void; +} + +export function GameBoard({ + contract, + account, + gameId, + currentRound, + onRoundPlayed, +}: GameBoardProps) { + const [allocations, setAllocations] = useState<[number, number, number, number, number]>([2, 2, 2, 2, 1]); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState(''); + const { addLog } = useTransactionLog(); + + function updateAllocation(trackIndex: number, value: number) { + const newAllocations = [...allocations] as [number, number, number, number, number]; + newAllocations[trackIndex] = value; + setAllocations(newAllocations); + } + + const total = allocations.reduce((sum, v) => sum + v, 0); + + // docs:start:submit-round + async function handleSubmitRound() { + if (total >= 10) { + setStatus(`Points must sum to less than 10 (currently ${total})`); + return; + } + + setLoading(true); + setStatus('Submitting your allocation (private transaction)...'); + addLog(`Round ${currentRound}: Submitting allocation [${allocations.join(', ')}]...`, 'pending'); + try { + addLog('Building private transaction proof...', 'pending'); + const receipt = await playRound(contract, account, gameId, currentRound, allocations); + addLog(`Round ${currentRound} submitted successfully`, 'success', receipt.receipt.txHash?.toString()); + setStatus('Round submitted!'); + setAllocations([2, 2, 2, 2, 1]); + onRoundPlayed(); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error submitting round: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + // docs:end:submit-round + + // docs:start:finish-and-finalize + async function handleFinishGame() { + setLoading(true); + setStatus('Revealing your total scores...'); + addLog('Revealing scores (finish_game)...', 'pending'); + try { + addLog('Reading private notes and computing totals...', 'pending'); + const receipt = await finishGame(contract, account, gameId); + addLog('Scores revealed successfully', 'success', receipt.receipt.txHash?.toString()); + setStatus('Scores revealed! Waiting for opponent to reveal, then finalize.'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error revealing scores: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + async function handleFinalizeGame() { + setLoading(true); + setStatus('Determining winner...'); + addLog('Finalizing game and determining winner...', 'pending'); + try { + const receipt = await finalizeGame(contract, account, gameId); + addLog('Game finalized! Winner determined.', 'success', receipt.receipt.txHash?.toString()); + setStatus('Game finalized! Winner determined.'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error finalizing game: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + // docs:end:finish-and-finalize + + const allRoundsPlayed = currentRound > TOTAL_ROUNDS; + + return ( +
+

{allRoundsPlayed ? 'All Rounds Played' : `Round ${currentRound} of ${TOTAL_ROUNDS}`}

+ {!allRoundsPlayed && ( +

Allocate up to {MAX_POINTS_PER_ROUND} points across 5 tracks. Your allocation is private.

+ )} + {status &&

{status}

} + + {!allRoundsPlayed && ( + <> +
+ {TRACK_NAMES.map((name, i) => ( +
+ + updateAllocation(i, Number(e.target.value))} + disabled={loading} + /> + {allocations[i]} pts +
+ ))} +
+ +

= 10 ? 'error' : ''}`}> + Total: {total} / {MAX_POINTS_PER_ROUND} max +

+ + + + )} + + {allRoundsPlayed && ( +
+ + +
+ )} +
+ ); +} diff --git a/docs/examples/webapp-tutorial/src/components/GameLobby.tsx b/docs/examples/webapp-tutorial/src/components/GameLobby.tsx new file mode 100644 index 000000000000..05af34336f25 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/GameLobby.tsx @@ -0,0 +1,154 @@ +// docs:start:game-lobby-imports +import React, { useState } from 'react'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { Wallet } from '@aztec/aztec.js/wallet'; +import type { PodRacingContract } from '../artifacts/PodRacing'; +import { deployContract, createGame, joinGame, attachToContract } from '../contract'; +import { useTransactionLog } from './TransactionLog'; + +interface GameLobbyProps { + wallet: Wallet; + account: AztecAddress; + onGameJoined: (contract: PodRacingContract, gameId: bigint) => void; +} +// docs:end:game-lobby-imports + +export function GameLobby({ wallet, account, onGameJoined }: GameLobbyProps) { + const [status, setStatus] = useState(''); + const [isCreating, setIsCreating] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [gameId, setGameId] = useState('1'); + const [joinGameIdInput, setJoinGameIdInput] = useState(''); + const [joinContractAddress, setJoinContractAddress] = useState(''); + const { addLog } = useTransactionLog(); + + // docs:start:handle-create + async function handleCreateGame() { + setIsCreating(true); + setStatus('Deploying Pod Racing contract...'); + addLog('Starting contract deployment...', 'pending'); + try { + let gId: bigint; + try { + gId = BigInt(gameId); + if (gId <= 0n) throw new Error('must be positive'); + } catch { + setStatus('Invalid game ID — enter a positive integer'); + setIsCreating(false); + return; + } + + addLog('Compiling and sending deployment transaction...', 'pending'); + const contract = await deployContract(wallet, account); + addLog(`Contract deployed at ${contract.address.toString()}`, 'success'); + + setStatus('Creating game...'); + addLog('Creating game...', 'pending'); + const receipt = await createGame(contract, account, gId); + addLog(`Game ${gId} created successfully`, 'success', receipt.receipt.txHash?.toString()); + + setStatus(`Game created! Share contract address: ${contract.address}`); + onGameJoined(contract, gId); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error: ${msg}`, 'error'); + } finally { + setIsCreating(false); + } + } + // docs:end:handle-create + + // docs:start:handle-join + async function handleJoinGame() { + if (!joinContractAddress || !joinGameIdInput) { + setStatus('Enter contract address and game ID'); + return; + } + setIsJoining(true); + setStatus('Joining game...'); + addLog('Attaching to existing contract...', 'pending'); + try { + let gId: bigint; + try { + gId = BigInt(joinGameIdInput); + if (gId <= 0n) throw new Error('must be positive'); + } catch { + setStatus('Invalid game ID — enter a positive integer'); + setIsJoining(false); + return; + } + + const contractAddr = AztecAddress.fromString(joinContractAddress); + const contract = await attachToContract( + wallet, + contractAddr + ); + addLog(`Attached to contract ${contractAddr.toString()}`, 'info'); + addLog(`Joining game ${gId}...`, 'pending'); + const receipt = await joinGame(contract, account, gId); + addLog(`Joined game ${gId} successfully`, 'success', receipt.receipt.txHash?.toString()); + + setStatus('Joined game!'); + onGameJoined(contract, gId); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Error joining game: ${msg}`, 'error'); + } finally { + setIsJoining(false); + } + } + // docs:end:handle-join + + return ( +
+

Game Lobby

+ {status &&

{status}

} + +
+

Create New Game

+ + +
+ +
+

Join Existing Game

+ + + +
+
+ ); +} diff --git a/docs/examples/webapp-tutorial/src/components/GameStatus.tsx b/docs/examples/webapp-tutorial/src/components/GameStatus.tsx new file mode 100644 index 000000000000..b0c652a645e3 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/GameStatus.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; + +interface GameStatusProps { + account: AztecAddress; + gameId: bigint; + currentRound: number; +} + +// docs:start:game-status-component +/** + * Displays the current game status. + * + * In the Pod Racing contract, round progress is tracked publicly + * (which round each player is on), but point allocations are private. + * The currentRound is tracked locally in React state and incremented + * after each successful play_round transaction. + */ +export function GameStatus({ account, gameId, currentRound }: GameStatusProps) { + const addr = account.toString(); + const display = `${addr.slice(0, 10)}...${addr.slice(-6)}`; + + return ( +
+

Game Status

+

Game ID: {gameId.toString()}

+

Playing as: {display}

+

Current Round: {currentRound} / 3

+

+ Your point allocations are stored as private notes. + Opponents cannot see your strategy until you reveal scores. +

+
+ ); +} +// docs:end:game-status-component diff --git a/docs/examples/webapp-tutorial/src/components/TxStatus.tsx b/docs/examples/webapp-tutorial/src/components/TxStatus.tsx new file mode 100644 index 000000000000..060a436c52f1 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/TxStatus.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +type TxState = 'idle' | 'sending' | 'proving' | 'confirmed' | 'error'; + +interface TxStatusProps { + state: TxState; + txHash?: string; + error?: string; +} + +// docs:start:tx-status-component +/** + * Displays the current transaction lifecycle stage. + * Transactions flow: send() -> proving -> confirmed (or error). + */ +export function TxStatus({ state, txHash, error }: TxStatusProps) { + if (state === 'idle') return null; + + const messages: Record = { + idle: '', + sending: 'Sending transaction...', + proving: 'Proving transaction (generating ZK proof)...', + confirmed: 'Transaction confirmed!', + error: `Transaction failed: ${error}`, + }; + + return ( +
+

{messages[state]}

+ {txHash &&

Tx: {txHash.slice(0, 10)}...

} +
+ ); +} +// docs:end:tx-status-component diff --git a/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx b/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx new file mode 100644 index 000000000000..ddcf03d62743 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx @@ -0,0 +1,198 @@ +// docs:start:wallet-connect-imports +import React, { useState, useEffect } from 'react'; +import type { Wallet, GrantedAccountsCapability } from '@aztec/aztec.js/wallet'; +import type { WalletProvider } from '@aztec/wallet-sdk/manager'; +import type { NetworkType } from '../config'; +import { EmbeddedWallet } from '../embedded-wallet'; +import { discoverWallets, connectToProvider, getAppCapabilities } from '../wallet-connection'; +import { getNodeUrl } from '../config'; +import { useTransactionLog } from './TransactionLog'; + +interface WalletConnectProps { + network: NetworkType; + onWalletConnected: (wallet: Wallet | EmbeddedWallet) => void; +} +// docs:end:wallet-connect-imports + +// docs:start:wallet-connect-component +export function WalletConnect({ network, onWalletConnected }: WalletConnectProps) { + const [status, setStatus] = useState(''); + const [providers, setProviders] = useState([]); + const [verificationEmojis, setVerificationEmojis] = useState(null); + const [testAccountIndex, setTestAccountIndex] = useState(0); + const [loading, setLoading] = useState(false); + const [connected, setConnected] = useState(false); + const [discoveryDone, setDiscoveryDone] = useState(false); + const { addLog } = useTransactionLog(); + + /** Connect using the embedded wallet with a pre-deployed test account */ + async function connectLocal() { + setLoading(true); + setStatus('Initializing PXE (this may take a moment)...'); + addLog('Initializing local PXE client...', 'pending'); + try { + const nodeUrl = getNodeUrl('local'); + addLog(`Connecting to node at ${nodeUrl}`, 'info'); + const wallet = await EmbeddedWallet.initialize(nodeUrl); + addLog('PXE initialized successfully', 'success'); + + setStatus('Connecting test account...'); + addLog(`Connecting test account #${testAccountIndex + 1}...`, 'pending'); + await wallet.connectTestAccount(testAccountIndex); + addLog(`Test account #${testAccountIndex + 1} connected`, 'success'); + + setStatus('Connected!'); + onWalletConnected(wallet); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Connection error: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + // docs:start:remote-connect + /** Discover and connect to a browser extension wallet */ + useEffect(() => { + if (network !== 'remote') return; + + setStatus('Discovering wallet extensions...'); + setDiscoveryDone(false); + const { cancel, done } = discoverWallets(31337, 'pod-racing', (found) => { + setProviders(found); + setStatus(`Found ${found.length} wallet(s)`); + }); + + let isMounted = true; + done.then(() => { + if (isMounted) setDiscoveryDone(true); + }).catch((err) => { + if (isMounted) setStatus(`Discovery error: ${err.message}`); + }); + + return () => { + isMounted = false; + cancel(); + }; + }, [network]); + + async function connectExtension(provider: WalletProvider) { + setLoading(true); + setStatus('Establishing secure channel...'); + try { + const { emojis, confirm } = await connectToProvider( + provider, + 'pod-racing' + ); + + // Show emojis for reference — the wallet extension is the authority + // that verifies the emojis match. confirm() is a local operation + // that creates the ExtensionWallet proxy (no message sent to extension). + setVerificationEmojis(emojis); + setStatus('Verify these emojis match in the wallet extension, then approve there.'); + + const wallet = await confirm(); + + // Request capabilities — the dApp declares all permissions it needs upfront. + // The extension shows an approval dialog; this call blocks until the user approves. + setStatus('Requesting permissions from wallet extension...'); + const manifest = getAppCapabilities(); + + const capabilities = await wallet.requestCapabilities(manifest); + setVerificationEmojis(null); + console.log('[WalletConnect] Granted capabilities:', capabilities); + + // Check if accounts were granted + const accountsCap = capabilities.granted.find( + (c): c is GrantedAccountsCapability => c.type === 'accounts' + ); + + if (!accountsCap?.accounts?.length) { + setStatus('No accounts granted. Please approve the capabilities request in the wallet extension.'); + setLoading(false); + return; + } + + setConnected(true); + setStatus('Connected!'); + addLog('Connected to extension wallet', 'success'); + onWalletConnected(wallet); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + setStatus(`Error: ${msg}`); + addLog(`Connection error: ${msg}`, 'error'); + } finally { + setLoading(false); + } + } + + // docs:end:remote-connect + + return ( +
+

Connect Wallet

+ {status &&

{status}

} + + {network === 'local' && ( +
+ + +
+ )} + + {network === 'remote' && !verificationEmojis && !connected && ( +
+ {providers.length === 0 && ( + discoveryDone + ?

No wallet extensions found. Install an Aztec wallet extension.

+ :

Looking for wallet extensions...

+ )} + {providers.map((provider, i) => ( + + ))} +
+ )} + + {verificationEmojis && ( +
+

Verify Connection

+

Check that these emojis match what your wallet extension shows, then approve there:

+
{verificationEmojis}
+
+ )} + +
+ ); +} +// docs:end:wallet-connect-component diff --git a/docs/examples/webapp-tutorial/src/config.ts b/docs/examples/webapp-tutorial/src/config.ts new file mode 100644 index 000000000000..423e7c4b613e --- /dev/null +++ b/docs/examples/webapp-tutorial/src/config.ts @@ -0,0 +1,33 @@ +// docs:start:config +import { createAztecNodeClient } from '@aztec/aztec.js/node'; +import { getPXEConfig } from '@aztec/pxe/config'; +import { createPXE } from '@aztec/pxe/client/lazy'; + + +export type NetworkType = 'local' | 'remote'; + +export function getNodeUrl(network: NetworkType): string { + if (network === 'local') { + return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; + } + // For remote networks, the wallet extension manages the node connection + return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; +} + +/** + * Creates an in-browser PXE instance connected to an Aztec node. + * PXE (Private eXecution Environment) runs locally and handles + * private state, note discovery, and transaction creation. + */ +export async function createLocalPXE(nodeUrl: string) { + const aztecNode = createAztecNodeClient(nodeUrl); + const config = getPXEConfig(); + config.l1Contracts = await aztecNode.getL1ContractAddresses(); + const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + config.proverEnabled = !isLocal; + const pxe = await createPXE(aztecNode, config, {}); + console.log('PXE connected to node at:', nodeUrl); + + return { pxe, aztecNode }; +} +// docs:end:config diff --git a/docs/examples/webapp-tutorial/src/contract.ts b/docs/examples/webapp-tutorial/src/contract.ts new file mode 100644 index 000000000000..00b44c343300 --- /dev/null +++ b/docs/examples/webapp-tutorial/src/contract.ts @@ -0,0 +1,120 @@ +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { Wallet } from '@aztec/aztec.js/wallet'; +// @ts-ignore — generated artifact, may not exist until compiled +import { PodRacingContract, PodRacingContractArtifact } from './artifacts/PodRacing'; +import { createSponsoredFeePayment } from './fees'; +import { EmbeddedWallet } from './embedded-wallet'; + +/** + * Deploys a new Pod Racing contract. + * The deployer becomes the game admin. + */ +export async function deployContract(wallet: Wallet, deployer: AztecAddress): Promise { + const paymentMethod = await createSponsoredFeePayment(); + const { contract } = await PodRacingContract.deploy(wallet, deployer) + .send({ from: deployer, fee: { paymentMethod } }); + + console.log('Pod Racing contract deployed at:', contract.address.toString()); + return contract; +} + +/** + * Attaches to an existing deployed Pod Racing contract. + * Registers the contract with PXE so private functions can execute locally. + */ +export async function attachToContract( + wallet: Wallet, + contractAddress: AztecAddress +) { + if (wallet instanceof EmbeddedWallet) { + await wallet.registerContractFromNode(contractAddress, PodRacingContractArtifact); + } + return PodRacingContract.at(contractAddress, wallet); +} + +/** + * Creates a new game with the given game_id. + */ +export async function createGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .create_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game created, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Joins an existing game as player2. + */ +export async function joinGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .join_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Joined game, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Allocates points to 5 tracks for a round (private transaction). + */ +export async function playRound( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint, + round: number, + tracks: [number, number, number, number, number] +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .play_round(gameId, round, tracks[0], tracks[1], tracks[2], tracks[3], tracks[4]) + .send({ from, fee: { paymentMethod } }); + + console.log('Round played, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Reveals your total scores per track after all rounds are played. + */ +export async function finishGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .finish_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game finished (scores revealed), tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} + +/** + * Determines the winner after both players have revealed. + */ +export async function finalizeGame( + contract: PodRacingContract, + from: AztecAddress, + gameId: bigint +) { + const paymentMethod = await createSponsoredFeePayment(); + const receipt = await contract.methods + .finalize_game(gameId) + .send({ from, fee: { paymentMethod } }); + + console.log('Game finalized, tx hash:', receipt.receipt.txHash.toString()); + return receipt; +} diff --git a/docs/examples/webapp-tutorial/src/embedded-wallet.ts b/docs/examples/webapp-tutorial/src/embedded-wallet.ts new file mode 100644 index 000000000000..969ccafd839e --- /dev/null +++ b/docs/examples/webapp-tutorial/src/embedded-wallet.ts @@ -0,0 +1,144 @@ +// docs:start:embedded-wallet-imports +import { type NoFrom, NO_FROM } 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 { SPONSORED_FPC_SALT } from '@aztec/constants'; +import { AccountFeePaymentMethodOptions } from '@aztec/entrypoints/account'; +import type { FieldsOf } from '@aztec/foundation/types'; +import { getInitialTestAccountsData } from '@aztec/accounts/testing/lazy'; +import type { ContractArtifact } from '@aztec/stdlib/abi'; +import { GasSettings } from '@aztec/stdlib/gas'; +import { type FeeOptions } from '@aztec/wallet-sdk/base-wallet'; +import { EmbeddedWallet as BaseEmbeddedWallet } from '@aztec/wallets/embedded'; +// docs:end:embedded-wallet-imports + +// docs:start:embedded-wallet-class +/** + * A tutorial wallet for local development. + * Extends the official EmbeddedWallet to add SponsoredFPC fee payment + * so users don't need to hold fee tokens. + * + * Inherits from the SDK's EmbeddedWallet which provides: + * - Account creation and persistence via WalletDB + * - Pre-simulation with gas estimation in sendTx + * - Automatic authwitness generation + * - Stub-account simulation (no expensive kernel proving) + */ +export class EmbeddedWallet extends BaseEmbeddedWallet { + connectedAccount: AztecAddress | null = null; + + // docs:start:fee-options + /** + * Uses SponsoredFPC for fee payment by default, so users + * don't need to hold fee tokens. + */ + override async completeFeeOptions( + from: AztecAddress | NoFrom, + feePayer?: AztecAddress, + gasSettings?: Partial>, + ): Promise { + const maxFeesPerGas = + gasSettings?.maxFeesPerGas ?? + (await this.aztecNode.getCurrentMinFees()).mul(1 + this.minFeePadding); + + let walletFeePaymentMethod; + let accountFeePaymentMethodOptions; + + if (!feePayer) { + const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); + walletFeePaymentMethod = new SponsoredFeePaymentMethod( + fpc.instance.address, + ); + if (from !== NO_FROM) { + accountFeePaymentMethodOptions = AccountFeePaymentMethodOptions.EXTERNAL; + } + } else if (from !== NO_FROM) { + accountFeePaymentMethodOptions = from.equals(feePayer) + ? AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM + : AccountFeePaymentMethodOptions.EXTERNAL; + } + + return { + gasSettings: GasSettings.default({ ...gasSettings, maxFeesPerGas }), + walletFeePaymentMethod, + accountFeePaymentMethodOptions, + }; + } + // docs:end:fee-options + + // docs:start:initialize + /** + * Creates a new EmbeddedWallet connected to the given Aztec node URL. + * Sets up an in-browser PXE and registers the SponsoredFPC contract. + */ + static async initialize(nodeUrl: string) { + const isLocal = + nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + const wallet = await EmbeddedWallet.create(nodeUrl, { + ephemeral: true, + pxeConfig: { proverEnabled: !isLocal }, + }); + + // Register SponsoredFPC so we can pay fees + const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); + await wallet.registerContract(fpc.instance, fpc.artifact); + + return wallet; + } + // docs:end:initialize + + static async #getSponsoredFPCContract() { + const { SponsoredFPCContractArtifact } = await import( + '@aztec/noir-contracts.js/SponsoredFPC' + ); + const instance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContractArtifact, + { salt: new Fr(SPONSORED_FPC_SALT) }, + ); + return { instance, artifact: SponsoredFPCContractArtifact }; + } + + getConnectedAccount() { + return this.connectedAccount; + } + + // docs:start:connect-test-account + /** + * Connects one of the pre-deployed test accounts available on the local network. + * Uses the inherited createSchnorrAccount which handles account creation, + * contract registration, and WalletDB persistence. + */ + async connectTestAccount(index: number) { + const testAccounts = await getInitialTestAccountsData(); + const accountData = testAccounts[index]; + + const accountManager = await this.createSchnorrAccount( + accountData.secret, + accountData.salt, + accountData.signingKey, + ); + + this.connectedAccount = accountManager.address; + return this.connectedAccount; + } + // docs:end:connect-test-account + + /** + * Fetches a contract instance from the Aztec node (onchain) and registers it + * with this wallet's PXE. Required before calling private functions on contracts + * deployed by another wallet/PXE. + */ + async registerContractFromNode( + address: AztecAddress, + artifact: ContractArtifact, + ) { + const instance = await this.aztecNode.getContract(address); + if (!instance) { + throw new Error(`Contract not found onchain at ${address}`); + } + await this.registerContract(instance, artifact); + } + // docs:end:embedded-wallet-class +} diff --git a/docs/examples/webapp-tutorial/src/fees.ts b/docs/examples/webapp-tutorial/src/fees.ts new file mode 100644 index 000000000000..98b85724821d --- /dev/null +++ b/docs/examples/webapp-tutorial/src/fees.ts @@ -0,0 +1,47 @@ +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 { SPONSORED_FPC_SALT } from '@aztec/constants'; +import type { PXE } from '@aztec/pxe/server'; + +// docs:start:get-sponsored-fpc +/** + * Returns the SponsoredFPC contract details. + * The SponsoredFPC (Fee Payment Contract) pays transaction fees on behalf of users. + * This is deployed at a well-known address derived from a fixed salt. + */ +export async function getSponsoredFPCContract() { + const { SponsoredFPCContractArtifact } = await import( + '@aztec/noir-contracts.js/SponsoredFPC' + ); + const instance = await getContractInstanceFromInstantiationParams( + SponsoredFPCContractArtifact, + { salt: new Fr(SPONSORED_FPC_SALT) } + ); + return { instance, artifact: SponsoredFPCContractArtifact }; +} +// docs:end:get-sponsored-fpc + +// docs:start:register-fpc +/** + * Registers the SponsoredFPC contract with PXE so it can be used for fee payment. + * This must be called before sending any transactions. + */ +export async function registerSponsoredFPC(pxe: PXE) { + const contract = await getSponsoredFPCContract(); + await pxe.registerContract(contract); + return contract.instance.address; +} +// docs:end:register-fpc + +// docs:start:create-fee-payment +/** + * Creates a SponsoredFeePaymentMethod that can be passed as the + * `paymentMethod` option when sending transactions. + */ +export async function createSponsoredFeePayment() { + const contract = await getSponsoredFPCContract(); + return new SponsoredFeePaymentMethod(contract.instance.address); +} +// docs:end:create-fee-payment diff --git a/docs/examples/webapp-tutorial/src/game-constants.ts b/docs/examples/webapp-tutorial/src/game-constants.ts new file mode 100644 index 000000000000..4e1ab1d2854a --- /dev/null +++ b/docs/examples/webapp-tutorial/src/game-constants.ts @@ -0,0 +1,5 @@ +/** Shared game constants used by all game UI components. */ + +export const TRACK_NAMES = ['Straight', 'Canyon', 'Asteroid Field', 'Nebula', 'Wormhole'] as const; +export const MAX_POINTS_PER_ROUND = 9; +export const TOTAL_ROUNDS = 3; diff --git a/docs/examples/webapp-tutorial/src/wallet-connection.ts b/docs/examples/webapp-tutorial/src/wallet-connection.ts new file mode 100644 index 000000000000..b279872be5ce --- /dev/null +++ b/docs/examples/webapp-tutorial/src/wallet-connection.ts @@ -0,0 +1,112 @@ +import { Fr } from '@aztec/aztec.js/fields'; +import type { Wallet, AppCapabilities } from '@aztec/aztec.js/wallet'; +import { WalletManager, type WalletProvider } from '@aztec/wallet-sdk/manager'; +import { hashToEmoji } from '@aztec/wallet-sdk/crypto'; + +// docs:start:wallet-sdk-types +export interface WalletDiscoveryState { + providers: WalletProvider[]; + selectedProvider: WalletProvider | null; + verificationEmojis: string | null; + wallet: Wallet | null; + status: 'idle' | 'discovering' | 'verifying' | 'connected' | 'error'; + error: string | null; +} + +export const initialWalletState: WalletDiscoveryState = { + providers: [], + selectedProvider: null, + verificationEmojis: null, + wallet: null, + status: 'idle', + error: null, +}; +// docs:end:wallet-sdk-types + +// docs:start:discover-wallets +/** + * Starts discovering available wallet extensions. + * Wallet extensions broadcast their availability via window.postMessage. + * Returns a cancel function and calls onUpdate with each discovered wallet. + */ +export function discoverWallets( + chainId: number, + appId: string, + onUpdate: (providers: WalletProvider[]) => void +): { cancel: () => void; done: Promise } { + const manager = WalletManager.configure({ + extensions: { enabled: true }, + }); + + const providers: WalletProvider[] = []; + + const discovery = manager.getAvailableWallets({ + chainInfo: { + chainId: new Fr(chainId), + version: new Fr(1), + }, + appId, + onWalletDiscovered: (provider) => { + // Deduplicate by wallet ID (StrictMode or remounts can cause duplicate discoveries) + if (providers.some(p => p.id === provider.id)) { + return; + } + providers.push(provider); + onUpdate([...providers]); + }, + }); + + return { + cancel: () => discovery.cancel(), + done: discovery.done, + }; +} +// docs:end:discover-wallets + +// docs:start:connect-wallet +/** + * Connects to a discovered wallet provider. + * This establishes a secure encrypted channel using ECDH key exchange. + * The returned emojis should be shown to the user for verification. + */ +export async function connectToProvider( + provider: WalletProvider, + appId: string +): Promise<{ + emojis: string; + confirm: () => Promise; + cancel: () => void; +}> { + console.log('[wallet-connection] Calling establishSecureChannel for provider:', provider.name); + const pending = await provider.establishSecureChannel(appId); + console.log('[wallet-connection] Secure channel established, verificationHash:', pending.verificationHash); + const emojis = hashToEmoji(pending.verificationHash); + console.log('[wallet-connection] Emojis:', emojis); + + return { + emojis, + confirm: () => pending.confirm(), + cancel: () => pending.cancel(), + }; +} +// docs:end:connect-wallet + +// docs:start:app-capabilities +export function getAppCapabilities(): AppCapabilities { + return { + version: '1.0', + metadata: { + name: 'Pod Racing', + version: '1.0.0', + description: 'Pod Racing game on Aztec', + url: window.location.origin, + }, + capabilities: [ + { type: 'accounts', canGet: true }, + { type: 'contracts', contracts: '*', canRegister: true, canGetMetadata: true }, + { type: 'simulation', transactions: { scope: '*' }, utilities: { scope: '*' } }, + { type: 'transaction', scope: '*' }, + ], + }; +} +// docs:end:app-capabilities diff --git a/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts b/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts new file mode 100644 index 000000000000..5aaf733f8bb6 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts @@ -0,0 +1,60 @@ +/** + * Shared account contract instantiation logic. + * + * Centralizes the key derivation + contract setup sequence used by + * account creation, deployment, and registration. + */ + +import { getAztecCore } from './aztec-imports'; + +/** + * Derives keys and instantiates a Schnorr account contract from a secret and salt. + * + * Returns everything needed for PXE registration and deployment: + * - secretFr / saltFr — field element versions of the inputs + * - publicKeys — derived public keys + * - signingKey — Schnorr signing key + * - accountContract — the SchnorrAccountContract instance + * - artifact — the contract artifact + * - instance — the contract instance with computed address + */ +export async function instantiateAccount(secret: string, salt: string) { + const { + Fr, + deriveKeys, + deriveSigningKey, + SchnorrAccountContract, + getContractInstanceFromInstantiationParams, + } = await getAztecCore(); + + const secretFr = Fr.fromString(secret); + const saltFr = Fr.fromString(salt); + + const { publicKeys } = await deriveKeys(secretFr); + const signingKey = deriveSigningKey(secretFr); + const accountContract = new SchnorrAccountContract(signingKey); + + const initInfo = await accountContract.getInitializationFunctionAndArgs(); + const { constructorName, constructorArgs } = initInfo ?? { + constructorName: undefined, + constructorArgs: undefined, + }; + const artifact = await accountContract.getContractArtifact(); + + const instance = await getContractInstanceFromInstantiationParams(artifact, { + constructorArtifact: constructorName, + constructorArgs, + salt: saltFr, + publicKeys, + }); + + return { + secretFr, + saltFr, + publicKeys, + signingKey, + accountContract, + artifact, + instance, + }; +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts b/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts new file mode 100644 index 000000000000..99c8795c1029 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts @@ -0,0 +1,125 @@ +/** + * Centralized lazy import cache for Aztec SDK modules. + * + * The offscreen document uses dynamic imports to keep startup fast — we don't + * want to load Barretenberg, Schnorr account contracts, etc. until the user + * actually triggers an operation. But the same imports were scattered across + * 5+ locations with no shared cache. + * + * This module loads all needed Aztec imports once, caches them, and provides + * a typed interface for consumers. The module system caches the underlying + * import() calls, but this provides a single entry point and avoids repeating + * the destructuring boilerplate. + */ + +/** Core imports needed for account operations (key derivation, contract setup) */ +export interface AztecCoreImports { + Fr: typeof import('@aztec/aztec.js/fields').Fr; + AztecAddress: typeof import('@aztec/aztec.js/addresses').AztecAddress; + deriveKeys: typeof import('@aztec/stdlib/keys').deriveKeys; + deriveSigningKey: typeof import('@aztec/stdlib/keys').deriveSigningKey; + SchnorrAccountContract: typeof import('@aztec/accounts/schnorr/lazy').SchnorrAccountContract; + getContractInstanceFromInstantiationParams: typeof import('@aztec/aztec.js/contracts').getContractInstanceFromInstantiationParams; + AccountManager: typeof import('@aztec/aztec.js/wallet').AccountManager; +} + +/** Additional imports for the wallet runtime (BaseWallet, serialization) */ +export interface AztecWalletImports extends AztecCoreImports { + BaseWallet: typeof import('@aztec/wallet-sdk/base-wallet').BaseWallet; + SignerlessAccount: typeof import('@aztec/aztec.js/account').SignerlessAccount; + WalletSchema: typeof import('@aztec/aztec.js/wallet').WalletSchema; + jsonStringify: typeof import('@aztec/foundation/json-rpc').jsonStringify; + schemaHasMethod: typeof import('@aztec/foundation/schemas').schemaHasMethod; +} + +/** Deploy-specific imports (fee payment, SponsoredFPC) */ +export interface AztecDeployImports extends AztecCoreImports { + SponsoredFeePaymentMethod: typeof import('@aztec/aztec.js/fee').SponsoredFeePaymentMethod; + SponsoredFPCContract: typeof import('@aztec/noir-contracts.js/SponsoredFPC').SponsoredFPCContract; + SPONSORED_FPC_SALT: typeof import('@aztec/constants').SPONSORED_FPC_SALT; +} + +let coreCache: AztecCoreImports | null = null; +let walletCache: AztecWalletImports | null = null; +let deployCache: AztecDeployImports | null = null; + +/** + * Loads the core Aztec imports needed for account operations. + * Cached after first call. + */ +export async function getAztecCore(): Promise { + if (coreCache) return coreCache; + + const [fields, addresses, keys, schnorr, contracts, wallet] = await Promise.all([ + import('@aztec/aztec.js/fields'), + import('@aztec/aztec.js/addresses'), + import('@aztec/stdlib/keys'), + import('@aztec/accounts/schnorr/lazy'), + import('@aztec/aztec.js/contracts'), + import('@aztec/aztec.js/wallet'), + ]); + + coreCache = { + Fr: fields.Fr, + AztecAddress: addresses.AztecAddress, + deriveKeys: keys.deriveKeys, + deriveSigningKey: keys.deriveSigningKey, + SchnorrAccountContract: schnorr.SchnorrAccountContract, + getContractInstanceFromInstantiationParams: contracts.getContractInstanceFromInstantiationParams, + AccountManager: wallet.AccountManager, + }; + + return coreCache; +} + +/** + * Loads the wallet runtime imports (core + BaseWallet, serialization). + * Cached after first call. + */ +export async function getAztecWallet(): Promise { + if (walletCache) return walletCache; + + const [core, bw, account, walletMod, jsonRpc, schemas] = await Promise.all([ + getAztecCore(), + import('@aztec/wallet-sdk/base-wallet'), + import('@aztec/aztec.js/account'), + import('@aztec/aztec.js/wallet'), + import('@aztec/foundation/json-rpc'), + import('@aztec/foundation/schemas'), + ]); + + walletCache = { + ...core, + BaseWallet: bw.BaseWallet, + SignerlessAccount: account.SignerlessAccount, + WalletSchema: walletMod.WalletSchema, + jsonStringify: jsonRpc.jsonStringify, + schemaHasMethod: schemas.schemaHasMethod, + }; + + return walletCache; +} + +/** + * Loads the deploy-specific imports (core + fee payment, SponsoredFPC). + * Cached after first call. + */ +export async function getAztecDeploy(): Promise { + if (deployCache) return deployCache; + + const [core, fee, sponsoredFpc, constants] = await Promise.all([ + getAztecCore(), + import('@aztec/aztec.js/fee'), + import('@aztec/noir-contracts.js/SponsoredFPC'), + import('@aztec/constants'), + ]); + + deployCache = { + ...core, + SponsoredFeePaymentMethod: fee.SponsoredFeePaymentMethod, + SponsoredFPCContract: sponsoredFpc.SponsoredFPCContract, + SPONSORED_FPC_SALT: constants.SPONSORED_FPC_SALT, + }; + + return deployCache; +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/background.ts b/docs/examples/webapp-tutorial/test-extension/src/background.ts new file mode 100644 index 000000000000..e3c1a3342aed --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/background.ts @@ -0,0 +1,1236 @@ +/** + * Background service worker for the Aztec Tutorial Wallet. + * + * Handles: + * - Wallet SDK protocol (discovery, key exchange, encrypted wallet method calls) + * - Offscreen document lifecycle management (with retry on teardown) (#15) + * - Message routing between content scripts and offscreen document + * - Background task tracking with push notifications to popup via ports + * - State persistence via chrome.storage.session (survives SW restart) (#8) + * - Auto-lock via chrome.alarms (#28) + */ + +import { + BackgroundConnectionHandler, + type BackgroundTransport, + type BackgroundConnectionCallbacks, + type ActiveSession, +} from '@aztec/wallet-sdk/extension/handlers'; + +import { WALLET_CONFIG, MessageTarget, MessageTypes, AUTO_LOCK_MINUTES, log } from './config'; +import { getErrorMessage } from './utils'; +import { STORAGE_KEYS } from './wallet/storage'; +import type { PendingTransaction, PendingSessionVerification, PendingCapabilities, BackgroundTask } from './shared-types'; + +// docs:start:offscreen-management +let offscreenCreating: Promise | null = null; + +/** + * Ensures the offscreen document exists. Creates it if needed. + * The offscreen document hosts the PXE and wallet implementation. + */ +async function ensureOffscreenDocument(): Promise { + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], + }); + + if (existingContexts.length > 0) { + return; + } + + if (offscreenCreating) { + await offscreenCreating; + return; + } + + const offscreenUrl = chrome.runtime.getURL('dist/offscreen.html'); + log.debug('[background] Creating offscreen document:', offscreenUrl); + + offscreenCreating = chrome.offscreen.createDocument({ + url: offscreenUrl, + reasons: [chrome.offscreen.Reason.WORKERS], + justification: 'Aztec PXE requires long-running WASM operations', + }); + + await offscreenCreating; + offscreenCreating = null; + log.debug('[background] Offscreen document created'); +} +// docs:end:offscreen-management + +// docs:start:send-to-offscreen +/** + * Persistent port to the offscreen document. + * Unlike chrome.runtime.sendMessage() (broadcast), a port gives us: + * - Point-to-point channel (no broadcast to all extension pages) + * - Automatic disconnect detection (offscreen teardown) + * - No `return true`/`false` landmine for async responses + */ +let offscreenPort: chrome.runtime.Port | null = null; +const pendingOffscreenCalls = new Map void; + reject: (error: Error) => void; + timer: ReturnType; +}>(); +let offscreenMessageId = 0; + +const OFFSCREEN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes + +function connectOffscreenPort() { + const port = chrome.runtime.connect({ name: 'offscreen' }); + offscreenPort = port; + + port.onMessage.addListener((message: any) => { + // Progress updates — relay to popup + if (message.type === 'task-progress') { + const runningTask = backgroundTasks.find((t) => t.status === 'running'); + if (runningTask) { + runningTask.progress = message.stage; + notifyPopup({ type: 'task-update', task: { ...runningTask } }); + } + return; + } + + // Request/response correlation + const pending = pendingOffscreenCalls.get(message.messageId); + if (!pending) return; + pendingOffscreenCalls.delete(message.messageId); + clearTimeout(pending.timer); + + if (message.success) { + pending.resolve(message.result); + } else { + pending.reject(new Error(message.error || 'Unknown error')); + } + }); + + port.onDisconnect.addListener(() => { + log.debug('[background] Offscreen port disconnected'); + offscreenPort = null; + // Reject all pending calls — sendToOffscreen will retry + for (const [id, pending] of pendingOffscreenCalls) { + clearTimeout(pending.timer); + pending.reject(new Error('Offscreen port disconnected')); + pendingOffscreenCalls.delete(id); + } + }); +} + +/** + * Sends a message to the offscreen document and waits for response. + * Uses a persistent port with request/response correlation via messageId. + * Retries once if the offscreen document was torn down. (#15) + */ +async function sendToOffscreen(message: any, _retried = false): Promise { + await ensureOffscreenDocument(); + + if (!offscreenPort) { + connectOffscreenPort(); + } + + const messageId = `off-${++offscreenMessageId}`; + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pendingOffscreenCalls.delete(messageId); + reject(new Error(`Offscreen call timed out: ${message.type}`)); + }, OFFSCREEN_TIMEOUT_MS); + + pendingOffscreenCalls.set(messageId, { resolve, reject, timer }); + + try { + if (!offscreenPort) { + throw new Error('Offscreen port not connected'); + } + offscreenPort.postMessage({ ...message, messageId }); + } catch (err: unknown) { + pendingOffscreenCalls.delete(messageId); + clearTimeout(timer); + + // Port may have disconnected — retry once + if (!_retried) { + log.warn('[background] Offscreen port send failed, retrying...'); + offscreenPort = null; + offscreenCreating = null; + sendToOffscreen(message, true).then(resolve, reject); + } else { + reject(err instanceof Error ? err : new Error(String(err))); + } + } + }); +} +// docs:end:send-to-offscreen + +// Store pending transactions, session verifications, and capability requests. +// Discovery and session tracking uses only the SDK handler. +let pendingTransactions: PendingTransaction[] = []; +let pendingSessionVerifications: PendingSessionVerification[] = []; +let pendingCapabilities: PendingCapabilities[] = []; +let walletUnlocked = false; + +/** Sessions that have had capabilities approved. Methods are blocked until this is set. */ +const capabilitiesApprovedSessions = new Set(); + +/** + * Messages queued while awaiting emoji verification. + * The extension must confirm the session before any wallet method calls are processed. + * This prevents the dApp from bypassing verification by sending messages immediately. + */ +const queuedMessages: Map = new Map(); + +/** + * Trusted origins persistence for auto-reconnect. (#30) + * + * When a user confirms emoji verification for a dApp, we remember the + * origin+appId in chrome.storage.local. On subsequent page refreshes, + * discovery and emoji verification are auto-approved (a fresh ECDH key + * exchange still happens every time for security). Disconnecting a site + * removes it from the trusted list. + */ +const TRUSTED_ORIGINS_KEY = 'aztec_trusted_origins'; + +interface TrustedOrigin { + origin: string; + appId: string; + trustedAt: number; + grantedCapabilities?: Array<{ type: string; [key: string]: any }>; +} + +async function getTrustedOrigins(): Promise { + const data = await chrome.storage.local.get(TRUSTED_ORIGINS_KEY); + return data[TRUSTED_ORIGINS_KEY] || []; +} + +async function addTrustedOrigin(origin: string, appId: string): Promise { + const trusted = await getTrustedOrigins(); + if (!trusted.some(t => t.origin === origin && t.appId === appId)) { + trusted.push({ origin, appId, trustedAt: Date.now() }); + await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: trusted }); + } +} + +async function removeTrustedOrigin(origin: string, appId: string): Promise { + const trusted = await getTrustedOrigins(); + const filtered = trusted.filter(t => !(t.origin === origin && t.appId === appId)); + await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: filtered }); +} + +async function isTrustedOrigin(origin: string, appId: string): Promise { + const trusted = await getTrustedOrigins(); + return trusted.some(t => t.origin === origin && t.appId === appId); +} + +async function getStoredCapabilities(origin: string, appId: string): Promise | null> { + const trusted = await getTrustedOrigins(); + const entry = trusted.find(t => t.origin === origin && t.appId === appId); + return entry?.grantedCapabilities ?? null; +} + +/** + * Persists critical state to chrome.storage.session. (#8) + * + * chrome.storage.session is: + * - Encrypted at rest + * - Scoped to the browser session (cleared when browser closes) + * - Survives service worker restarts (unlike in-memory variables) + * + * We persist: walletUnlocked, pendingTransactions. + * We do NOT persist: CryptoKey (lives in offscreen), active SDK sessions + * (the SDK handler can't be serialized; dApps must reconnect after SW restart). + */ +async function persistState(): Promise { + try { + await chrome.storage.session.set({ + sw_walletUnlocked: walletUnlocked, + sw_pendingTransactions: pendingTransactions, + }); + } catch (err) { + log.warn('[background] Failed to persist state:', err); + } +} + +async function restoreState(): Promise { + try { + const data = await chrome.storage.session.get([ + 'sw_walletUnlocked', + 'sw_pendingTransactions', + ]); + walletUnlocked = data.sw_walletUnlocked ?? false; + pendingTransactions = data.sw_pendingTransactions ?? []; + log.debug('[background] Restored state: unlocked =', walletUnlocked, ', pendingTx =', pendingTransactions.length); + + // Validate: if the offscreen document was torn down, the cached master key is gone. + // Probe offscreen to confirm — if it fails, the wallet needs re-unlock. + if (walletUnlocked) { + try { + await sendToOffscreen({ type: MessageTypes.GET_ACCOUNTS }); + } catch { + log.warn('[background] Offscreen unreachable after restore — marking wallet as locked'); + walletUnlocked = false; + await persistState(); + } + } + } catch (err) { + log.warn('[background] Failed to restore state:', err); + } +} + +/** + * Background task tracker for long-running operations. + * Tasks survive popup close/reopen. The popup receives real-time updates + * via a persistent port connection. + */ +let backgroundTasks: BackgroundTask[] = []; + +function startBackgroundTask(type: string, promise: Promise): string { + const id = `${type}-${Date.now()}`; + const task: BackgroundTask = { id, type, status: 'running', startedAt: Date.now() }; + backgroundTasks.push(task); + + promise + .then((result) => { + task.status = 'success'; + task.result = result; + notifyPopup({ type: 'task-update', task: { ...task } }); + }) + .catch((error) => { + task.status = 'error'; + task.error = getErrorMessage(error); + notifyPopup({ type: 'task-update', task: { ...task } }); + }); + + notifyPopup({ type: 'task-update', task: { ...task } }); + return id; +} + +/** + * Cleans up completed tasks after 5 minutes. (#13) + * 5 minutes gives the popup time to see completed tasks even if it was closed briefly. + */ +function cleanupTasks() { + const FIVE_MINUTES = 5 * 60 * 1000; + const cutoff = Date.now() - FIVE_MINUTES; + backgroundTasks = backgroundTasks.filter( + (t) => t.status === 'running' || t.startedAt > cutoff + ); +} + +/** + * Persistent port connection to the popup. + * Allows the background to push real-time updates without polling. + * The port disconnects automatically when the popup closes. + */ +let popupPort: chrome.runtime.Port | null = null; + +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== 'popup') return; + + log.debug('[background] Popup connected'); + popupPort = port; + + port.onDisconnect.addListener(() => { + log.debug('[background] Popup disconnected'); + popupPort = null; + }); + + // Send current state immediately on connect + pushStateToPopup(); +}); + +function notifyPopup(message: any) { + if (popupPort) { + try { + popupPort.postMessage(message); + } catch { + popupPort = null; + } + } +} + +function pushStateToPopup() { + notifyPopup({ type: 'state', data: getFullState() }); +} + +/** + * Opens the popup via chrome.windows.create() — works without a user gesture. + * Used for discovery requests which are triggered by content script messages + * where chrome.action.openPopup() silently no-ops. + */ +function openPopupWindow() { + if (popupPort) { + pushStateToPopup(); + return; + } + chrome.windows.create({ + url: chrome.runtime.getURL('popup/popup.html'), + type: 'popup', + width: 400, + height: 600, + focused: true, + }).catch((err) => { + log.error('[background] Failed to create popup window:', err); + }); +} + +/** + * Opens the popup via chrome.action.openPopup() with windows.create() fallback. + * Used for flows that originate from a user gesture in the popup (tx approval, + * session verification) where openPopup() works reliably. + */ +function openPopupWithFallback() { + if (popupPort) { + pushStateToPopup(); + return; + } + chrome.action.openPopup().catch(() => { + chrome.windows.create({ + url: chrome.runtime.getURL('popup/popup.html'), + type: 'popup', + width: 400, + height: 600, + focused: true, + }); + }); +} + +function getFullState() { + cleanupTasks(); + const connectedSites = handler.getActiveSessions().map((s) => ({ + sessionId: s.sessionId, + origin: s.origin, + appId: s.appId, + connectedAt: s.connectedAt, + })); + + return { + discoveries: handler.getPendingDiscoveries(), + transactions: pendingTransactions, + pendingSessionVerifications, + pendingCapabilities, + connectedSites, + tasks: backgroundTasks, + }; +} + +/** + * Auto-lock via chrome.alarms. (#28) + * Resets the timer on every popup interaction. + */ +const AUTO_LOCK_ALARM = 'aztec-auto-lock'; + +function resetAutoLockTimer() { + if (walletUnlocked) { + chrome.alarms.create(AUTO_LOCK_ALARM, { delayInMinutes: AUTO_LOCK_MINUTES }); + } +} + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === AUTO_LOCK_ALARM) { + log.debug('[background] Auto-lock triggered'); + walletUnlocked = false; + await persistState(); + // Tell offscreen to clear the cached CryptoKey + try { + await sendToOffscreen({ type: MessageTypes.LOCK_WALLET }); + } catch { + // Offscreen may not exist yet + } + pushStateToPopup(); + } +}); + +/** + * Updates the extension badge to show pending items count. + */ +function updateBadge() { + const count = pendingTransactions.length + handler.getPendingDiscoveryCount() + pendingSessionVerifications.length + pendingCapabilities.length; + chrome.action.setBadgeText({ text: count > 0 ? count.toString() : '' }); + chrome.action.setBadgeBackgroundColor({ color: '#FF6B00' }); + pushStateToPopup(); + persistState(); +} + +// docs:start:transport +const transport: BackgroundTransport = { + sendToTab: (tabId, message) => { + log.debug('[background] sendToTab:', tabId, message.type, message.sessionId); + chrome.tabs.sendMessage(tabId, message); + }, + addContentListener: (handler) => { + chrome.runtime.onMessage.addListener((message, sender) => { + // Skip targeted messages (popup, offscreen), storage proxy, and progress updates + if (message.target) return; + if (message.type === 'storage-get' || message.type === 'storage-set') return; + + log.debug('[background] Content message received:', message.origin, message.type, 'from tab:', sender.tab?.id); + handler(message, { + tab: sender.tab ? { id: sender.tab.id, url: sender.tab.url } : undefined, + }); + }); + }, +}; +// docs:end:transport + +/** + * Returns the list of accounts grantable to dApps — only the active account, + * or the first account as fallback. Used by both processWalletMessage (for + * getAccounts/getRegisteredAccounts filtering) and APPROVE_CAPABILITIES. + */ +async function getGrantableAccounts(): Promise> { + const [accountsData, activeData] = await Promise.all([ + chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS), + chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT), + ]); + const allAccounts = accountsData[STORAGE_KEYS.ACCOUNTS] || []; + const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; + const activeAccount = allAccounts.find((a: any) => a.address === activeAddress); + if (activeAccount) { + return [{ alias: activeAccount.alias, item: activeAccount.address }]; + } + return allAccounts.slice(0, 1).map((a: any) => ({ alias: a.alias, item: a.address })); +} + +/** + * Processes a wallet method call from the ExtensionWallet proxy. + * Extracted so it can be called from onWalletMessage and when flushing the queue. + */ +async function processWalletMessage(session: ActiveSession, message: any) { + // Allow requestCapabilities to pass through (it's the mechanism for getting approved). + // Block all other wallet methods until capabilities have been approved for this session. + if (message.type !== 'requestCapabilities' && !capabilitiesApprovedSessions.has(session.sessionId)) { + log.warn('[background] Rejecting wallet method before capabilities approved:', message.type); + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + error: 'Capabilities not yet approved. Call requestCapabilities() first.', + walletId: WALLET_CONFIG.walletId, + }); + return; + } + + // docs:start:approval-check + // sendTx always requires approval — it's a state-changing operation. + // batch requires approval only if it contains a sendTx (e.g. BatchCall.send()). + // Read-only batches (simulateTx, executeUtility, etc.) auto-execute. + const needsApproval = + message.type === 'sendTx' || + (message.type === 'batch' && + Array.isArray(message.args?.[0]) && + message.args[0].some((m: any) => m.name === 'sendTx')); + + if (needsApproval) { + // Extract `from` address from the method args: + // - sendTx args: [executionPayload, sendOptions] → from is in sendOptions + // - batch args: [methodsArray] → find the sendTx entry and get from from its opts + let from = ''; + if (message.type === 'sendTx') { + from = message.args?.[1]?.from?.toString?.() || ''; + } else if (message.type === 'batch') { + const sendTxMethod = message.args[0].find((m: any) => m.name === 'sendTx'); + from = sendTxMethod?.args?.[1]?.from?.toString?.() || ''; + } + + const pending: PendingTransaction = { + sessionId: session.sessionId, + messageId: message.messageId, + method: message.type, + args: message.args, + from, + origin: session.origin, + timestamp: Date.now(), + }; + + pendingTransactions.push(pending); + updateBadge(); + log.debug('[background] Transaction pending approval:', pending.method); + + openPopupWithFallback(); + return; + } + // docs:end:approval-check + + // Capability requests require user approval — push to pending state. + if (message.type === 'requestCapabilities') { + // Auto-approve for trusted origins if the requested capabilities match what was previously granted + const requestedManifest = message.args?.[0]; + const requestedCaps: any[] = requestedManifest?.capabilities || []; + if (await isTrustedOrigin(session.origin, session.appId)) { + const savedCaps = await getStoredCapabilities(session.origin, session.appId); + if (savedCaps) { + // Verify the requested capability types match the previously approved set + const requestedTypes = requestedCaps.map((c: any) => c.type).sort(); + const savedTypes = savedCaps.map((c: any) => c.type).sort(); + const capsMatch = requestedTypes.length === savedTypes.length && + requestedTypes.every((t: string, i: number) => t === savedTypes[i]); + + if (capsMatch) { + const grantedAccounts = await getGrantableAccounts(); + const granted = savedCaps.map((cap: any) => { + if (cap.type === 'accounts') { + return { ...cap, accounts: grantedAccounts }; + } + return { ...cap }; + }); + + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + result: { + version: '1.0', + granted, + wallet: { name: WALLET_CONFIG.walletName, version: WALLET_CONFIG.walletVersion }, + }, + walletId: WALLET_CONFIG.walletId, + }); + capabilitiesApprovedSessions.add(session.sessionId); + log.debug('[background] Auto-approved capabilities for trusted origin:', session.origin); + return; + } + log.debug('[background] Requested capabilities differ from saved — requiring approval'); + } + } + + // Not trusted, no saved caps, or requested caps differ — require user approval + const manifest = requestedManifest; + const pending: PendingCapabilities = { + sessionId: session.sessionId, + messageId: message.messageId, + origin: session.origin, + appMetadata: manifest?.metadata || { name: 'Unknown App', version: '0.0.0' }, + capabilities: requestedCaps, + timestamp: Date.now(), + }; + pendingCapabilities.push(pending); + updateBadge(); + openPopupWithFallback(); + return; + } + + // All other wallet methods — execute silently (no task banner). + // These are read-only or background calls (executeUtility, simulateTx, batch + // without sendTx, getAccounts, etc.) that don't need user visibility. + (async () => { + let result = await sendToOffscreen({ + type: MessageTypes.WALLET_METHOD, + method: message.type, + args: message.args, + }); + + // MetaMask-like behavior: return only the active account for getAccounts + if (message.type === 'getAccounts' || message.type === 'getRegisteredAccounts') { + const activeData = await chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT); + const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; + if (activeAddress && Array.isArray(result)) { + const activeAccount = result.find((acc: any) => + acc.item === activeAddress || acc.item?.toString() === activeAddress + ); + result = activeAccount ? [activeAccount] : result; + } + } + + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + result, + walletId: WALLET_CONFIG.walletId, + }); + })().catch(async (error: any) => { + log.error('[background] Error handling wallet message:', message.type, error); + await handler.sendResponse(session.sessionId, { + messageId: message.messageId, + error: error.message, + walletId: WALLET_CONFIG.walletId, + }); + }); +} + +// docs:start:callbacks +const callbacks: BackgroundConnectionCallbacks = { + onPendingDiscovery: async (discovery) => { + log.debug('[background] Pending discovery:', discovery.requestId, 'from', discovery.origin); + + // Clean up stale sessions from this tab (e.g. page refresh creates a new + // discovery while the old session is still in activeSessions). + for (const session of handler.getActiveSessions()) { + if (session.tabId === discovery.tabId) { + log.debug('[background] Terminating stale session for tab:', discovery.tabId, session.sessionId); + capabilitiesApprovedSessions.delete(session.sessionId); + queuedMessages.delete(session.sessionId); + handler.terminateSession(session.sessionId); + } + } + + // Deduplicate: reject any existing discovery from the same tab + const existing = handler.getPendingDiscoveries().find( + (d) => d.tabId === discovery.tabId && d.requestId !== discovery.requestId + ); + if (existing) { + handler.rejectDiscovery(existing.requestId); + } + + // Auto-approve if origin is already trusted (reconnection after page refresh) + if (await isTrustedOrigin(discovery.origin, discovery.appId)) { + log.debug('[background] Auto-approving trusted origin:', discovery.origin); + handler.approveDiscovery(discovery.requestId); + return; + } + + updateBadge(); + + openPopupWithFallback(); + }, + + onSessionEstablished: async (session: ActiveSession) => { + log.debug('[background] Session established:', session.sessionId); + + // Auto-confirm if origin is already trusted (skip emoji verification) + if (await isTrustedOrigin(session.origin, session.appId)) { + log.debug('[background] Auto-confirming trusted session:', session.sessionId); + + // Pre-approve capabilities if previously granted (enables seamless reconnect) + const savedCaps = await getStoredCapabilities(session.origin, session.appId); + if (savedCaps) { + capabilitiesApprovedSessions.add(session.sessionId); + } + + // Flush any queued messages immediately (same logic as CONFIRM_SESSION handler) + const queued = queuedMessages.get(session.sessionId) ?? []; + queuedMessages.delete(session.sessionId); + for (const { session: s, message: msg } of queued) { + processWalletMessage(s, msg); + } + pushStateToPopup(); + return; + } + + // New origin — require emoji verification + log.debug('[background] Awaiting emoji verification for:', session.sessionId); + // SDK automatically removes the discovery when key exchange completes. + // Show emojis in approvals so user can compare with the webapp + pendingSessionVerifications.push({ + sessionId: session.sessionId, + origin: session.origin, + appId: session.appId, + verificationHash: session.verificationHash, + timestamp: Date.now(), + }); + updateBadge(); + + // Only open popup if not already connected — calling openPopup() on an + // already-open popup rejects, and the fallback creates a second window + // that steals the popupPort from the original. + if (!popupPort) { + openPopupWithFallback(); + } + + pushStateToPopup(); + }, + + // docs:start:on-wallet-message + /** + * Handles wallet method calls from the ExtensionWallet proxy. + * Messages are queued while emoji verification is pending — the extension + * user must confirm before any dApp calls are processed. + */ + onWalletMessage: async (session: ActiveSession, message: any) => { + log.debug('[background] Wallet message:', message.type, 'from session:', session.sessionId); + + // Block wallet messages until the user confirms emoji verification in the extension. + // The dApp's calls (e.g. getAccounts) will wait until the extension user approves. + const awaitingVerification = pendingSessionVerifications.some( + (v) => v.sessionId === session.sessionId + ); + if (awaitingVerification) { + log.debug('[background] Session awaiting verification, queuing message:', message.type); + const queue = queuedMessages.get(session.sessionId) ?? []; + queue.push({ session, message }); + queuedMessages.set(session.sessionId, queue); + return; + } + + await processWalletMessage(session, message); + }, + // docs:end:on-wallet-message +}; +// docs:end:callbacks + +const handler = new BackgroundConnectionHandler(WALLET_CONFIG, transport, callbacks); +handler.initialize(); + +// Clean up sessions when a tab is closed +chrome.tabs.onRemoved.addListener((tabId) => { + const sessions = handler.getActiveSessions().filter((s) => s.tabId === tabId); + for (const session of sessions) { + capabilitiesApprovedSessions.delete(session.sessionId); + queuedMessages.delete(session.sessionId); + } + handler.terminateForTab(tabId); + pushStateToPopup(); +}); + +// docs:start:popup-messages +/** + * Handle messages from popup and offscreen for approvals and account management. + */ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + /** + * Storage proxy for the offscreen document. (#7) + * Validates that the request comes from the extension itself (not content scripts or external). + */ + if (message.type === 'storage-get' || message.type === 'storage-set') { + // Security: only allow storage proxy from extension pages (offscreen, popup) (#7) + // Content scripts have sender.tab set; extension pages (offscreen, popup) do not. + if (sender.tab) { + log.warn('[background] Rejected storage proxy from content script, tab:', sender.tab.id); + sendResponse({ success: false, error: 'Storage proxy not allowed from content scripts' }); + return false; + } + + if (message.type === 'storage-get') { + chrome.storage.local.get(message.key).then((result) => { + sendResponse({ success: true, result: result[message.key] }); + }).catch((err) => { + sendResponse({ success: false, error: err.message }); + }); + } else { + chrome.storage.local.set(message.data).then(() => { + sendResponse({ success: true }); + }).catch((err) => { + sendResponse({ success: false, error: err.message }); + }); + } + return true; // async response (#23) + } + + if (message.target !== MessageTarget.BACKGROUND) { + return false; + } + + log.debug('[background] Popup message:', message.type); + + // Reset auto-lock on any popup interaction (#28) + resetAutoLockTimer(); + + switch (message.type) { + case MessageTypes.APPROVE_CONNECTION: { + handler.approveDiscovery(message.requestId); + updateBadge(); + sendResponse({ success: true }); + return false; // sync response (#23) + } + + case MessageTypes.REJECT_CONNECTION: { + handler.rejectDiscovery(message.requestId); + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.APPROVE_TRANSACTION: { + const pending = pendingTransactions.find( + (t) => t.messageId === message.messageId + ); + if (pending) { + pendingTransactions = pendingTransactions.filter( + (t) => t.messageId !== message.messageId + ); + updateBadge(); + + const taskId = startBackgroundTask(`tx:${pending.method}`, + handleTransactionApproval(pending) + ); + sendResponse({ success: true, result: { taskId } }); + } else { + sendResponse({ success: false, error: 'Transaction not found' }); + } + return false; + } + + case MessageTypes.REJECT_TRANSACTION: { + const pending = pendingTransactions.find( + (t) => t.messageId === message.messageId + ); + if (pending) { + handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + error: 'Transaction rejected by user', + walletId: WALLET_CONFIG.walletId, + }); + + pendingTransactions = pendingTransactions.filter( + (t) => t.messageId !== message.messageId + ); + updateBadge(); + } + sendResponse({ success: true }); + return false; + } + + case MessageTypes.APPROVE_CAPABILITIES: { + const pending = pendingCapabilities.find( + (c) => c.messageId === message.messageId + ); + if (pending) { + pendingCapabilities = pendingCapabilities.filter( + (c) => c.messageId !== message.messageId + ); + + // Build granted capabilities using the shared active-account helper + getGrantableAccounts().then((grantedAccounts) => { + const granted = pending.capabilities.map((cap: any) => { + if (cap.type === 'accounts') { + return { ...cap, accounts: grantedAccounts }; + } + return { ...cap }; + }); + + return handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result: { + version: '1.0', + granted, + wallet: { + name: WALLET_CONFIG.walletName, + version: WALLET_CONFIG.walletVersion, + }, + }, + walletId: WALLET_CONFIG.walletId, + }); + }).then(async () => { + capabilitiesApprovedSessions.add(pending.sessionId); + + // Persist granted capabilities for auto-reconnect + const approvedSession = handler.getSession(pending.sessionId); + if (approvedSession) { + const trusted = await getTrustedOrigins(); + const entry = trusted.find(t => t.origin === approvedSession.origin && t.appId === approvedSession.appId); + if (entry) { + entry.grantedCapabilities = pending.capabilities.map((cap: any) => ({ ...cap })); + await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: trusted }); + } + } + + updateBadge(); + sendResponse({ success: true }); + }).catch((err) => { + log.error('[background] Failed to approve capabilities:', err); + sendResponse({ success: false, error: getErrorMessage(err) }); + }); + } else { + sendResponse({ success: false, error: 'Capability request not found' }); + return false; + } + return true; + } + + case MessageTypes.REJECT_CAPABILITIES: { + const pending = pendingCapabilities.find( + (c) => c.messageId === message.messageId + ); + if (pending) { + pendingCapabilities = pendingCapabilities.filter( + (c) => c.messageId !== message.messageId + ); + + handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result: { + version: '1.0', + granted: [], + wallet: { + name: WALLET_CONFIG.walletName, + version: WALLET_CONFIG.walletVersion, + }, + }, + walletId: WALLET_CONFIG.walletId, + }); + + updateBadge(); + } + sendResponse({ success: true }); + return false; + } + + case MessageTypes.CONFIRM_SESSION: { + // User confirmed emojis match — session is now fully active. + // Flush any wallet messages that were queued while awaiting verification. + pendingSessionVerifications = pendingSessionVerifications.filter( + (v) => v.sessionId !== message.sessionId + ); + const queued = queuedMessages.get(message.sessionId) ?? []; + queuedMessages.delete(message.sessionId); + for (const { session, message: msg } of queued) { + log.debug('[background] Flushing queued message:', msg.type); + processWalletMessage(session, msg); + } + + // Remember this origin as trusted for future reconnections (#30) + const confirmedSession = handler.getSession(message.sessionId); + if (confirmedSession) { + addTrustedOrigin(confirmedSession.origin, confirmedSession.appId); + } + + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.REJECT_SESSION: { + // User rejected emoji verification — reject queued messages and terminate the session. + pendingSessionVerifications = pendingSessionVerifications.filter( + (v) => v.sessionId !== message.sessionId + ); + const rejected = queuedMessages.get(message.sessionId) ?? []; + queuedMessages.delete(message.sessionId); + for (const { session, message: msg } of rejected) { + handler.sendResponse(session.sessionId, { + messageId: msg.messageId, + error: 'Session verification rejected by user', + walletId: WALLET_CONFIG.walletId, + }); + } + handler.terminateSession(message.sessionId); + updateBadge(); + sendResponse({ success: true }); + return false; + } + + case MessageTypes.DISCONNECT_SESSION: { + // (#29) Allow users to disconnect a specific dApp session + // Remove from trusted origins so next connection requires full approval (#30) + const disconnectedSession = handler.getSession(message.sessionId); + if (disconnectedSession) { + removeTrustedOrigin(disconnectedSession.origin, disconnectedSession.appId); + } + capabilitiesApprovedSessions.delete(message.sessionId); + handler.terminateSession(message.sessionId); + pushStateToPopup(); + sendResponse({ success: true }); + return false; + } + + case 'getPendingItems': { + sendResponse({ + success: true, + result: getFullState(), + }); + return false; + } + + case MessageTypes.GET_ACCOUNTS: { + chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS) + .then((data) => { + const accounts = (data[STORAGE_KEYS.ACCOUNTS] || []).map((acc: any) => ({ + address: acc.address, + alias: acc.alias, + isDeployed: acc.isDeployed, + })); + sendResponse({ success: true, result: accounts }); + }) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; // async (#23) + } + + case MessageTypes.GET_ACTIVE_ACCOUNT: { + chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT) + .then((data) => { + sendResponse({ success: true, result: data[STORAGE_KEYS.ACTIVE_ACCOUNT] || null }); + }) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; + } + + case MessageTypes.SET_ACTIVE_ACCOUNT: { + chrome.storage.local.set({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: message.address }) + .then(() => sendResponse({ success: true })) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; + } + + case MessageTypes.UNLOCK_WALLET: { + const taskId = startBackgroundTask('unlock', + sendToOffscreen({ + type: MessageTypes.UNLOCK_WALLET, + password: message.password, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.GET_WALLET_STATUS: { + chrome.storage.local.get(STORAGE_KEYS.PASSWORD_DATA) + .then((data) => { + sendResponse({ + success: true, + result: { + unlocked: walletUnlocked, + hasPassword: !!data[STORAGE_KEYS.PASSWORD_DATA], + }, + }); + }) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; + } + + case MessageTypes.SETUP_PASSWORD: { + const taskId = startBackgroundTask('setup-password', + sendToOffscreen({ + type: MessageTypes.SETUP_PASSWORD, + password: message.password, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.MARK_DEPLOYED: { + chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS) + .then((data) => { + const accounts = data[STORAGE_KEYS.ACCOUNTS] || []; + const account = accounts.find((a: any) => a.address === message.address); + if (account) { + account.isDeployed = true; + return chrome.storage.local.set({ [STORAGE_KEYS.ACCOUNTS]: accounts }); + } + }) + .then(() => sendResponse({ success: true, result: { success: true } })) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; + } + + case MessageTypes.CREATE_ACCOUNT: { + const taskId = startBackgroundTask('create-account', + sendToOffscreen({ + type: MessageTypes.CREATE_ACCOUNT, + alias: message.alias, + }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.DEPLOY_ACCOUNT: { + const taskId = startBackgroundTask('deploy-account', + sendToOffscreen({ + type: MessageTypes.DEPLOY_ACCOUNT, + address: message.address, + }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.EXPORT_WALLET: { + const taskId = startBackgroundTask('export-wallet', + sendToOffscreen({ type: MessageTypes.EXPORT_WALLET }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + case MessageTypes.IMPORT_WALLET: { + // Wipe wallet data from chrome.storage.local and lock the wallet + chrome.storage.local.remove([STORAGE_KEYS.ACCOUNTS, STORAGE_KEYS.PASSWORD_DATA, STORAGE_KEYS.ACTIVE_ACCOUNT]); + walletUnlocked = false; + persistState(); + // Tell offscreen to clear cached key + sendToOffscreen({ type: MessageTypes.LOCK_WALLET }).catch(() => {}); + sendResponse({ success: true, result: { success: true } }); + return false; + } + + case MessageTypes.IMPORT_WALLET_ACCOUNTS: { + const taskId = startBackgroundTask('import-wallet-accounts', + sendToOffscreen({ + type: MessageTypes.IMPORT_WALLET_ACCOUNTS, + accounts: message.accounts, + activeAccount: message.activeAccount, + }).then((result) => { + walletUnlocked = true; + persistState(); + resetAutoLockTimer(); + return result; + }) + ); + sendResponse({ success: true, result: { taskId } }); + return false; + } + + default: { + log.warn('[background] Unknown message type:', message.type); + return false; + } + } +}); + +async function handleTransactionApproval( + pending: PendingTransaction +): Promise { + try { + const result = await sendToOffscreen({ + type: MessageTypes.WALLET_METHOD, + method: pending.method, + args: pending.args, + }); + + await handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + result, + walletId: WALLET_CONFIG.walletId, + }); + + return result; + } catch (error: any) { + log.error('[background] Transaction approval failed:', pending.method, error); + await handler.sendResponse(pending.sessionId, { + messageId: pending.messageId, + error: error.message, + walletId: WALLET_CONFIG.walletId, + }); + throw error; + } +} +// docs:end:popup-messages + +/** + * Extension lifecycle handlers. (#17) + */ +chrome.runtime.onInstalled.addListener((details) => { + log.debug('[background] Extension installed/updated:', details.reason); + + // Clear stale pending state — sessions don't survive extension reload + pendingTransactions = []; + pendingSessionVerifications = []; + pendingCapabilities = []; + queuedMessages.clear(); + capabilitiesApprovedSessions.clear(); + persistState(); + + if (details.reason === 'install') { + // First install — nothing to migrate + log.debug('[background] First install, no migration needed'); + } else if (details.reason === 'update') { + // Version update — could add migration logic here + log.debug('[background] Updated from', details.previousVersion); + } +}); + +// Restore state on service worker startup (#8) +restoreState().then(() => { + log.debug('[background] Service worker initialized'); +}); + +// Eagerly preload the offscreen document so WASM and PXE deps are warm +ensureOffscreenDocument().then(() => { + log.debug('[background] Offscreen document preloaded'); +}).catch((err) => { + log.error('[background] Offscreen preload failed (will retry on demand):', err); +}); diff --git a/docs/examples/webapp-tutorial/test-extension/src/config.ts b/docs/examples/webapp-tutorial/test-extension/src/config.ts new file mode 100644 index 000000000000..00c7e3e00eee --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/config.ts @@ -0,0 +1,96 @@ +// docs:start:wallet-config +/** + * Configuration for the Aztec Tutorial Wallet extension. + * Uses SponsoredFPC for fee payment. + */ + +/** Aztec node URL — defaults to a local sandbox. */ +export const NODE_URL = 'http://localhost:8080'; + +/** Current @aztec/* package version, injected at build time by Vite. */ +declare const __AZTEC_PACKAGES_VERSION__: string; +export const AZTEC_PACKAGES_VERSION: string = + typeof __AZTEC_PACKAGES_VERSION__ !== 'undefined' ? __AZTEC_PACKAGES_VERSION__ : 'unknown'; + +/** Wallet identification for the SDK protocol */ +export const WALLET_CONFIG = { + walletId: 'aztec-tutorial-wallet', + walletName: 'Aztec Tutorial Wallet', + walletVersion: '1.0.0', + walletIcon: 'data:image/svg+xml,🔮', +}; + +/** Auto-lock timeout in minutes. The wallet locks after this period of inactivity. (#28) */ +export const AUTO_LOCK_MINUTES = 15; + +/** Message types for internal extension communication */ +export const MessageTypes = { + // Account management + GET_ACCOUNTS: 'get-accounts', + MARK_DEPLOYED: 'mark-deployed', + + // Full account creation in extension (uses Barretenberg) + CREATE_ACCOUNT: 'create-account', + DEPLOY_ACCOUNT: 'deploy-account', + + // Master password + wallet unlock + SETUP_PASSWORD: 'setup-password', + UNLOCK_WALLET: 'unlock-wallet', + GET_WALLET_STATUS: 'get-wallet-status', + + // PXE operations + INIT_PXE: 'init-pxe', + REGISTER_ACCOUNT: 'register-account', + + // Active account management + GET_ACTIVE_ACCOUNT: 'get-active-account', + SET_ACTIVE_ACCOUNT: 'set-active-account', + + // Wallet export/import + EXPORT_WALLET: 'export-wallet', + IMPORT_WALLET: 'import-wallet', + IMPORT_WALLET_ACCOUNTS: 'import-wallet-accounts', + + // Wallet SDK protocol — dispatches to BaseWallet + WALLET_METHOD: 'wallet-method', + + // Auto-lock + LOCK_WALLET: 'lock-wallet', + + // Popup -> Background + APPROVE_CONNECTION: 'approve-connection', + REJECT_CONNECTION: 'reject-connection', + APPROVE_TRANSACTION: 'approve-transaction', + REJECT_TRANSACTION: 'reject-transaction', + CONFIRM_SESSION: 'confirm-session', + REJECT_SESSION: 'reject-session', + DISCONNECT_SESSION: 'disconnect-session', + APPROVE_CAPABILITIES: 'approve-capabilities', + REJECT_CAPABILITIES: 'reject-capabilities', +} as const; + +/** Union type of all message type values — use for exhaustive checking. */ +export type MessageType = (typeof MessageTypes)[keyof typeof MessageTypes]; + +/** Targets for chrome.runtime messages */ +export const MessageTarget = { + OFFSCREEN: 'offscreen', + POPUP: 'popup', + BACKGROUND: 'background', +} as const; + + +/** + * Conditional logging. (#26) + * Strips verbose logs in production while keeping errors visible. + * Set DEBUG=true in the build to enable verbose logging. + */ +const DEBUG = process.env.NODE_ENV !== 'production'; // Toggle via build environment + +export const log = { + debug: (...args: unknown[]) => { if (DEBUG) console.log(...args); }, + info: (...args: unknown[]) => { if (DEBUG) console.info(...args); }, + warn: (...args: unknown[]) => console.warn(...args), + error: (...args: unknown[]) => console.error(...args), +}; +// docs:end:wallet-config diff --git a/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts b/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts new file mode 100644 index 000000000000..51e8863edb22 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts @@ -0,0 +1,771 @@ +/** Offscreen document for the Aztec Tutorial Wallet. */ + +// CRITICAL: console-intercept MUST be the very first import. +// Pino (used by PXE) captures `console.info` at logger-creation time. +// By overriding it in a separate module imported first, ES module execution +// order guarantees the override is in place before any pino logger is created. +import { onConsoleInfo } from './console-intercept'; + +import { NODE_URL, MessageTypes, AZTEC_PACKAGES_VERSION, log } from '../config'; +import type { WalletExportData } from '../shared-types'; +import { + createAccount, + getAccounts, + getAccountSecret, + markDeployed, + storeAccount, + getActiveAccount, + setActiveAccount, +} from '../wallet/wallet-impl'; +import type { PXE } from '@aztec/pxe/client/lazy'; +import type { Account } from '@aztec/aztec.js/account'; +import type { AztecNode } from '@aztec/aztec.js/node'; +import { createAztecNodeClient } from '@aztec/aztec.js/node'; +import { getPXEConfig } from '@aztec/pxe/config'; +import { createPXE } from '@aztec/pxe/client/lazy'; + +// ============================================================================ +// CRITICAL: Enable SharedArrayBuffer support for Barretenberg WASM +// ============================================================================ +// +// Chrome extensions have SharedArrayBuffer available but crossOriginIsolated=false. +// bb.js checks crossOriginIsolated to decide which WASM binary to load and +// whether to create shared WebAssembly.Memory. +// +// This patches the main thread. Worker files are patched at build time by the +// patchWorkersCrossOriginIsolated Vite plugin (see vite.extension.config.ts). +// ============================================================================ +if (typeof SharedArrayBuffer !== 'undefined' && !(globalThis as any).crossOriginIsolated) { + Object.defineProperty(globalThis, 'crossOriginIsolated', { + value: true, + writable: false, + configurable: true, + }); +} + +import { getChromeRuntime, getErrorMessage } from '../utils'; +import { getAztecCore, getAztecWallet, getAztecDeploy } from '../aztec-imports'; +import { instantiateAccount } from '../account-utils'; + +const chromeRuntime = getChromeRuntime(); +log.debug('[offscreen] Offscreen document loaded. Storage proxied through background. Barretenberg enabled via crossOriginIsolated patch.'); + +/** + * Port connection from the background script. + * Set when the background connects via chrome.runtime.connect({ name: 'offscreen' }). + */ +let backgroundPort: chrome.runtime.Port | null = null; + +/** + * Sends a progress update to the background script for display in the popup. + * Fire-and-forget — uses the persistent port instead of broadcast sendMessage. + */ +function reportProgress(stage: string) { + log.debug('[offscreen] Progress:', stage); + backgroundPort?.postMessage({ type: 'task-progress', stage }); +} + +/** + * PXE log matchers — match pino browser log messages and relay as progress updates. + * + * Pino browser mode with `asObject: false` calls: + * console.info(bindingsObj, dataObj, messageString) + * where bindingsObj contains `{ module: 'pxe:service' }` etc. + * + * The interception is set up in console-intercept.ts (imported first) so that + * pino captures our wrapped console.info, not the original. + */ +const PXE_STAGE_MATCHERS: Array<{ module: string; pattern: RegExp; stage: string }> = [ + { module: 'pxe:service', pattern: /^Simulating transaction/, stage: 'Simulating transaction...' }, + { module: 'pxe:service', pattern: /^Simulation completed/, stage: 'Simulation complete, proving...' }, + { module: 'pxe:private-kernel-execution-prover', pattern: /^Private kernel witness generation/, stage: 'Kernel witness generated, creating proof...' }, + { module: 'pxe:bb:wasm:bundle', pattern: /^Generating ClientIVC proof/, stage: 'Generating ZK proof (this takes a while)...' }, + { module: 'pxe:bb:wasm:bundle', pattern: /^Generated ClientIVC proof/, stage: 'Proof generated, sending...' }, + { module: 'wallet-sdk:base_wallet', pattern: /^Sent transaction/, stage: 'Transaction sent, awaiting confirmation...' }, +]; + +onConsoleInfo((args) => { + const bindings = args.find((a) => typeof a === 'object' && a !== null && typeof a.module === 'string'); + const message = [...args].reverse().find((a) => typeof a === 'string'); + + if (bindings && message) { + for (const matcher of PXE_STAGE_MATCHERS) { + if (bindings.module === matcher.module && matcher.pattern.test(message)) { + reportProgress(matcher.stage); + break; + } + } + } +}); + +/** + * Master CryptoKey — cached in memory after unlock. (#2) + * + * This is a non-extractable AES-GCM CryptoKey derived from the user's password + * via PBKDF2. The raw password string is NEVER stored; only this opaque key + * object is kept in memory. Even if an attacker gets a reference to this object, + * they cannot extract the underlying key material (WebCrypto enforces this). + */ +let cachedMasterKey: CryptoKey | null = null; + +function getCachedMasterKey(): CryptoKey { + if (!cachedMasterKey) { + throw new Error('Wallet is locked. Please unlock first.'); + } + return cachedMasterKey; +} + +/** Clear the cached key (used for auto-lock). */ +export function clearCachedKey(): void { + cachedMasterKey = null; +} + +// docs:start:pxe-instance +/** + * PXE + node — lazily initialized as a pair, with dedup on the inflight promise. + */ +let pxeState: { pxe: PXE; node: AztecNode } | null = null; +let pxeInitializing: Promise<{ pxe: PXE; node: AztecNode }> | null = null; + +async function ensurePXE(nodeUrl: string = NODE_URL): Promise<{ pxe: PXE; node: AztecNode }> { + if (pxeState) return pxeState; + if (pxeInitializing) return pxeInitializing; + + log.debug('[offscreen] Initializing PXE with node:', nodeUrl); + pxeInitializing = (async () => { + try { + const node = createAztecNodeClient(nodeUrl); + const config = getPXEConfig(); + config.l1Contracts = await node.getL1ContractAddresses(); + const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); + config.proverEnabled = !isLocal; + + const pxe = await createPXE(node, config, {}); + log.debug('[offscreen] PXE initialized, connected to node at:', nodeUrl); + + pxeState = { pxe, node }; + return pxeState; + } finally { + pxeInitializing = null; // Always clear so a retry can re-attempt + } + })(); + + return pxeInitializing; +} +// docs:end:pxe-instance + +// docs:start:wallet-instance +/** Single wallet class used for all operations. (#18, #20) */ + +import type { BaseWallet } from '@aztec/wallet-sdk/base-wallet'; + +/** + * The wallet instance holds a BaseWallet subclass with an additional + * registerAccount method for tracking which accounts we can sign for. + * BaseWallet is dynamically imported at runtime; using `import type` gives + * us the type without a runtime dependency. (#20) + */ +type OffscreenWalletType = BaseWallet & { registerAccount(address: string, account: Account): void }; + +let walletInstance: OffscreenWalletType | null = null; + +/** + * Creates a SponsoredFPC contract instance from its artifact and well-known salt. + * Shared between OffscreenWallet.ensureSponsoredFPC() and handleDeployAccount(). + */ +async function getSponsoredFPCInstance() { + const { Fr, SponsoredFPCContract, SPONSORED_FPC_SALT, getContractInstanceFromInstantiationParams } = await getAztecDeploy(); + return getContractInstanceFromInstantiationParams( + SponsoredFPCContract.artifact, + { salt: new Fr(SPONSORED_FPC_SALT) }, + ); +} + +async function getWallet() { + if (walletInstance) return walletInstance; + + const { BaseWallet, AztecAddress, SignerlessAccount } = await getAztecWallet(); + const { pxe, node } = await ensurePXE(); + + // AccountFeePaymentMethodOptions.EXTERNAL = 0 — fee is paid by an external FPC + const EXTERNAL_FEE_PAYMENT = 0; + + class OffscreenWallet extends BaseWallet { + protected minFeePadding = 1.0; // 100% padding for fee estimation variance + private accounts: Map = new Map(); + private sponsoredFPCAddress: any | null = null; + + constructor(pxeInstance: PXE, aztecNode: AztecNode) { + super(pxeInstance, aztecNode); + } + + registerAccount(address: string, account: Account) { + this.accounts.set(address, account); + } + + protected async getAccountFromAddress(address: any): Promise { + if (address.equals(AztecAddress.ZERO)) { + return new SignerlessAccount(); + } + const key = address.toString(); + const account = this.accounts.get(key); + if (!account) { + throw new Error(`Account not found for address: ${key}`); + } + return account; + } + + async getAccounts() { + return Array.from(this.accounts.entries()).map(([, acc]) => ({ + alias: '', + item: acc.getAddress(), + })); + } + + /** Lazily registers the SponsoredFPC contract and caches its address. */ + private async ensureSponsoredFPC() { + if (this.sponsoredFPCAddress) return this.sponsoredFPCAddress; + const { SponsoredFPCContract } = await getAztecDeploy(); + const sponsoredFPCInstance = await getSponsoredFPCInstance(); + await this.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); + this.sponsoredFPCAddress = sponsoredFPCInstance.address; + return this.sponsoredFPCAddress; + } + + // docs:start:complete-fee-options + /** + * Always uses SponsoredFPC for fee payment, mirroring the deployment flow. + * The tutorial wallet doesn't hold fee juice, so every tx is sponsor-paid. + * + * If the execution payload already has a feePayer (e.g. DeployAccountMethod + * embeds SponsoredFPC in its own payload), we skip injecting a wallet-level + * payment method to avoid calling sponsor_unconditionally() twice, which + * would trigger "Cannot enter the revertible phase twice". + */ + protected async completeFeeOptions(from: any, feePayer?: any, gasSettings?: any) { + const base = await super.completeFeeOptions(from, feePayer, gasSettings); + // If the payload already includes a fee payer, don't inject another one + if (feePayer) { + return { + ...base, + accountFeePaymentMethodOptions: EXTERNAL_FEE_PAYMENT, + }; + } + const address = await this.ensureSponsoredFPC(); + const { SponsoredFeePaymentMethod } = await getAztecDeploy(); + return { + ...base, + walletFeePaymentMethod: new SponsoredFeePaymentMethod(address), + accountFeePaymentMethodOptions: EXTERNAL_FEE_PAYMENT, + }; + } + // docs:end:complete-fee-options + + /** + * Overrides sendTx to auto-extract auth witnesses from offchain effects. + * + * dApps like gregoswap don't explicitly create auth witnesses. Instead, they + * expect the wallet to handle it: simulate with a stub account (which passes + * all auth checks), extract the authorization requests emitted by + * `#[authorize_once]` in Noir contracts, sign them, and include them in the + * real transaction. + */ + async sendTx(executionPayload: any, opts: any): Promise { + if (executionPayload.authWitnesses.length === 0 && opts.from && !opts.from.equals(AztecAddress.ZERO)) { + try { + await this.extractAndInjectAuthWitnesses(executionPayload, opts.from, opts.fee?.gasSettings); + } catch (err: any) { + log.error('[offscreen] Auth witness extraction failed, proceeding without:', err.message, err.stack); + } + } + return super.sendTx(executionPayload, opts); + } + + /** + * Simulates the tx with a stub account to collect offchain effects, + * parses CallAuthorizationRequest objects, and creates real auth witnesses. + */ + private async extractAndInjectAuthWitnesses(executionPayload: any, from: any, feeGasSettings?: any) { + const { Fr, getContractInstanceFromInstantiationParams } = await getAztecCore(); + + // Step 1: Create a stub account that passes all auth checks unconditionally + log.info('[offscreen] Step 1: Loading stub account module...'); + const realAccount = await this.getAccountFromAddress(from); + const originalAddress = realAccount.getCompleteAddress(); + log.info('[offscreen] Got complete address:', originalAddress.address.toString()); + + const { createStubAccount, getStubAccountContractArtifact } = await import('@aztec/accounts/stub/lazy'); + log.info('[offscreen] Loaded @aztec/accounts/stub/lazy'); + + const stubArtifact = await getStubAccountContractArtifact(); + log.info('[offscreen] Loaded stub artifact:', stubArtifact.name); + + const stubAccount = createStubAccount(originalAddress); + const stubInstance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random() }); + log.info('[offscreen] Created stub account and instance'); + + // Step 2: Simulate with the stub account swapped in via PXE overrides + log.info('[offscreen] Step 2: Simulating tx with stub account...'); + const feeOptions = await this.completeFeeOptions(from, executionPayload.feePayer, feeGasSettings); + const chainInfo = await this.getChainInfo(); + const txRequest = await stubAccount.createTxExecutionRequest( + executionPayload, + feeOptions.gasSettings, + chainInfo, + { txNonce: Fr.random(), cancellable: false, feePaymentMethodOptions: feeOptions.accountFeePaymentMethodOptions }, + ); + log.info('[offscreen] Created tx execution request, simulating...'); + + const simResult = await this.pxe.simulateTx(txRequest, { + simulatePublic: true, + skipTxValidation: true, + skipFeeEnforcement: true, + overrides: { contracts: { [from.toString()]: { instance: stubInstance, artifact: stubArtifact } } }, + scopes: [from], + }); + log.info('[offscreen] Simulation succeeded'); + + // Step 3: Extract auth witness requests from offchain effects + log.info('[offscreen] Step 3: Extracting offchain effects...'); + const { collectOffchainEffects } = await import('@aztec/stdlib/tx'); + const { CallAuthorizationRequest } = await import('@aztec/aztec.js/authorization'); + + if (!simResult.privateExecutionResult) { + log.warn('[offscreen] No privateExecutionResult in simulation result'); + return; + } + + const effects = collectOffchainEffects(simResult.privateExecutionResult); + log.info(`[offscreen] Found ${effects.length} offchain effect(s)`); + + // Pre-filter by CallAuthorizationRequest selector (matching e2e test pattern) + const callAuthSelector = await CallAuthorizationRequest.getSelector(); + const authEffects = effects.filter((e: any) => + e.data.length > 0 && e.data[0].equals(callAuthSelector.toField()), + ); + log.info(`[offscreen] ${authEffects.length} are CallAuthorizationRequest(s)`); + + // Step 4: Create auth witnesses from parsed authorization requests + let count = 0; + for (const effect of authEffects) { + const authRequest = await CallAuthorizationRequest.fromFields(effect.data); + log.info(`[offscreen] Auth request: consumer=${effect.contractAddress.toString()}, innerHash=${authRequest.innerHash.toString()}`); + const wit = await this.createAuthWit(from, { + consumer: effect.contractAddress, + innerHash: authRequest.innerHash, + }); + executionPayload.authWitnesses.push(wit); + count++; + log.info(`[offscreen] Created auth witness #${count}: messageHash=${wit.requestHash.toString()}`); + } + + log.info(`[offscreen] Auth witness extraction complete: ${count} witness(es) from ${effects.length} effect(s)`); + } + } + + walletInstance = new OffscreenWallet(pxe, node); + return walletInstance; +} +// docs:end:wallet-instance + +// docs:start:message-handler +/** + * Handles messages from the background script via a persistent port. + * The background connects with chrome.runtime.connect({ name: 'offscreen' }). + * Each message includes a messageId for request/response correlation. + */ +chromeRuntime.runtime.onConnect.addListener((port: chrome.runtime.Port) => { + if (port.name !== 'offscreen') return; + + log.debug('[offscreen] Background port connected'); + backgroundPort = port; + + port.onMessage.addListener((message: any) => { + log.debug('[offscreen] Received message:', message.type); + + handleMessage(message) + .then((result) => { + log.debug('[offscreen] Sending response for:', message.type); + port.postMessage({ messageId: message.messageId, success: true, result }); + }) + .catch((error: unknown) => { + const msg = getErrorMessage(error); + log.error('[offscreen] Error:', msg, error); + port.postMessage({ messageId: message.messageId, success: false, error: msg }); + }); + }); + + port.onDisconnect.addListener(() => { + log.debug('[offscreen] Background port disconnected'); + backgroundPort = null; + }); +}); + +async function handleMessage(message: any): Promise { + switch (message.type) { + case MessageTypes.GET_ACCOUNTS: + return handleGetAccounts(); + + case MessageTypes.MARK_DEPLOYED: + return handleMarkDeployed(message.address); + + case MessageTypes.WALLET_METHOD: + return handleWalletMethod(message.method, message.args); + + case MessageTypes.SETUP_PASSWORD: + return handleSetupPassword(message.password); + + case MessageTypes.CREATE_ACCOUNT: + return handleCreateAccount(message.alias); + + case MessageTypes.DEPLOY_ACCOUNT: + return handleDeployAccount(message.address); + + case MessageTypes.UNLOCK_WALLET: + return handleUnlockWallet(message.password); + + case MessageTypes.INIT_PXE: + return handleInitPXE(message.nodeUrl); + + case MessageTypes.REGISTER_ACCOUNT: + return handleRegisterAccount(message.address, message.secret, message.salt); + + case MessageTypes.EXPORT_WALLET: + return handleExportWallet(); + + case MessageTypes.IMPORT_WALLET_ACCOUNTS: + return handleImportWalletAccounts(message.accounts, message.activeAccount); + + // Lock the wallet (clear cached key) — used by auto-lock (#28) + case MessageTypes.LOCK_WALLET: + cachedMasterKey = null; + walletInstance = null; + return { success: true }; + + default: + throw new Error(`Unknown message type: ${message.type}`); + } +} +// docs:end:message-handler + +async function handleGetAccounts() { + return getAccounts(); +} + +async function handleMarkDeployed(address: string) { + await markDeployed(address); + return { success: true }; +} + +// docs:start:wallet-method-handler +/** + * Handles wallet method calls from the ExtensionWallet proxy via the SDK protocol. + * + * Serialization notes: + * 1. ARGS: Arrive as plain JSON. We use WalletSchema to parse them back into + * proper Aztec types (AztecAddress, Fr, ExecutionPayload, etc.). + * 2. RESULT: Contains class instances that lose prototypes through Chrome messaging. + * We serialize with jsonStringify before returning. + */ +async function handleWalletMethod(method: string, args: any[]): Promise { + log.debug('[offscreen] Handling wallet method:', method); + + const wallet = await getWallet(); + + // Dynamic dispatch: the wallet protocol sends method names as strings. + // Cast to Record for dynamic access since TypeScript can't know the method at compile time. + const walletObj = wallet as unknown as Record any>; + if (typeof walletObj[method] !== 'function') { + throw new Error(`Unknown wallet method: ${method}`); + } + + const { WalletSchema, jsonStringify, schemaHasMethod } = await getAztecWallet(); + + // Parse args through WalletSchema to reconstruct proper Aztec types (Buffer, Fr, etc.) + // from their JSON representations. The schema's .parameters() returns a zod tuple that + // requires all positional elements even if some are optional. Pad with undefined so the + // tuple length matches and the parse succeeds. + let parsedArgs: any[] = args || []; + if (schemaHasMethod(WalletSchema, method)) { + const schema = WalletSchema[method as keyof typeof WalletSchema]; + const paramSchema = schema.parameters(); + const expectedLength = (paramSchema as any)?._def?.items?.length ?? 0; + const paddedArgs = [...(args || [])]; + while (paddedArgs.length < expectedLength) { + paddedArgs.push(undefined); + } + try { + parsedArgs = await paramSchema.parseAsync(paddedArgs); + } catch (parseErr: any) { + log.warn('[offscreen] Args parse warning for', method, ':', parseErr.message); + parsedArgs = args || []; + } + } + + // Report initial progress for long-running methods so the popup shows something + // before the PXE log matchers kick in + const longRunningMethods = ['sendTx', 'simulateTx', 'profileTx']; + if (longRunningMethods.includes(method)) { + reportProgress(`Starting ${method}...`); + } + + const result = await walletObj[method](...parsedArgs); + + // Serialize to JSON-safe format before returning through Chrome messaging + const jsonSafe = JSON.parse(jsonStringify(result)); + log.debug('[offscreen] Wallet method completed:', method); + return jsonSafe; +} +// docs:end:wallet-method-handler + +/** + * Sets the master password for the first time. (#1, #2) + * Returns the CryptoKey (which we cache), never stores the password. + */ +async function handleSetupPassword(password: string) { + log.debug('[offscreen] Setting up master password'); + const { setupPassword, hasPassword: checkHasPassword } = await import('../wallet/storage'); + const exists = await checkHasPassword(); + if (exists) { + throw new Error('Master password already set'); + } + cachedMasterKey = await setupPassword(password); + // password string goes out of scope here — only the CryptoKey survives + return { success: true }; +} + +/** + * Creates an account using the cached master CryptoKey. (#2, #3) + */ +async function handleCreateAccount(alias: string) { + const masterKey = getCachedMasterKey(); + log.debug('[offscreen] Creating account with alias:', alias); + const result = await createAccount(masterKey, alias); + log.debug('[offscreen] Account created:', result.address); + return { address: result.address }; +} + +// docs:start:deploy-account +/** + * Deploys an account contract onchain using SponsoredFPC for fee payment. + * Uses the cached master CryptoKey to decrypt the account secret. (#2, #4) + */ +async function handleDeployAccount(address: string) { + const masterKey = getCachedMasterKey(); + log.debug('[offscreen] Deploying account:', address); + + // 1. Decrypt the account secret + reportProgress('Decrypting account secret...'); + const secretData = await getAccountSecret(address, masterKey); + if (!secretData) { + throw new Error(`Account not found: ${address}`); + } + + // 2. Ensure PXE is initialized (needed by the wallet) + reportProgress('Connecting to PXE...'); + await ensurePXE(); + + // 3. Register account with PXE and wallet (shared with unlock flow) + reportProgress('Registering account contract...'); + const { accountManager } = await registerAccountInWallet(address, secretData.secret, secretData.salt); + + // 4. Register SponsoredFPC contract with PXE (shared helper) + reportProgress('Registering fee payment contract...'); + const { AztecAddress, SponsoredFeePaymentMethod, SponsoredFPCContract } = await getAztecDeploy(); + const sponsoredFPCInstance = await getSponsoredFPCInstance(); + const wallet = await getWallet(); + await wallet.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); + + // 5. Deploy with SponsoredFPC fee payment. + // PXE log matchers (PXE_STAGE_MATCHERS) provide granular progress updates + // (simulating → proving → proof generated → sending → awaiting confirmation). + reportProgress('Starting deploy tx...'); + + const paymentMethod = new SponsoredFeePaymentMethod(sponsoredFPCInstance.address); + const deployMethod = await accountManager.getDeployMethod(); + const receipt = await deployMethod.send({ + from: AztecAddress.ZERO, + fee: { paymentMethod }, + wait: { timeout: 2400 }, + }); + + // 6. Mark deployed in storage + await markDeployed(address); + reportProgress('Deploy complete!'); + + log.debug('[offscreen] Account deployed:', address, 'txHash:', receipt.txHash?.toString()); + return { success: true, txHash: receipt.txHash?.toString() }; +} +// docs:end:deploy-account + +// docs:start:load-accounts +/** + * Unlocks the wallet: verifies password, caches CryptoKey, initializes PXE, + * registers all stored accounts. (#1, #2) + * + * After unlock: + * - cachedMasterKey holds the non-extractable CryptoKey + * - The password string is discarded (goes out of scope) + * - All accounts are registered with PXE and the BaseWallet + */ +async function handleUnlockWallet(password: string) { + log.debug('[offscreen] Unlocking wallet...'); + + reportProgress('Verifying password...'); + const { verifyAndDeriveMasterKey, hasPassword: checkHasPassword } = await import('../wallet/storage'); + + if (await checkHasPassword()) { + const masterKey = await verifyAndDeriveMasterKey(password); + if (!masterKey) { + throw new Error('Incorrect password'); + } + cachedMasterKey = masterKey; + // password string goes out of scope — only the CryptoKey survives + } else { + throw new Error('No password set. Please set up your wallet first.'); + } + + // Initialize PXE + reportProgress('Initializing PXE (loading WASM)...'); + await ensurePXE(); + log.debug('[offscreen] PXE initialized for unlock'); + + // Register all stored accounts + const storedAccounts = await getAccounts(); + reportProgress(`Registering ${storedAccounts.length} account(s)...`); + log.debug('[offscreen] Registering', storedAccounts.length, 'accounts with PXE'); + + const failedAccounts: string[] = []; + for (const account of storedAccounts) { + try { + const secretData = await getAccountSecret(account.address, cachedMasterKey); + if (secretData) { + await registerAccountInWallet(account.address, secretData.secret, secretData.salt); + log.debug('[offscreen] Registered account:', account.address); + } + } catch (err: any) { + log.error('[offscreen] Failed to register account:', account.address, err.message); + failedAccounts.push(account.address); + // Continue with remaining accounts — partial unlock is better than full lockout + } + } + + if (failedAccounts.length === storedAccounts.length && storedAccounts.length > 0) { + // ALL accounts failed — password is likely wrong + cachedMasterKey = null; + throw new Error('Failed to unlock: wrong password or corrupted data'); + } + if (failedAccounts.length > 0) { + log.warn('[offscreen] Partial unlock:', failedAccounts.length, 'account(s) failed to register'); + } + + log.debug('[offscreen] Wallet unlocked,', storedAccounts.length - failedAccounts.length, 'of', storedAccounts.length, 'accounts registered'); + return { success: true }; +} +// docs:end:load-accounts + +/** + * Registers an account with both PXE and the BaseWallet. + * Returns the AccountManager so callers (e.g. deploy) can use it for further operations. + */ +async function registerAccountInWallet(address: string, secret: string, salt: string) { + const { secretFr, saltFr, accountContract, artifact, instance } = + await instantiateAccount(secret, salt); + + const { AccountManager } = await getAztecCore(); + + const wallet = await getWallet(); + await wallet.registerContract(instance, artifact, secretFr); + + const accountManager = await AccountManager.create(wallet, secretFr, accountContract, saltFr); + const account = await accountManager.getAccount(); + wallet.registerAccount(address, account); + + log.debug('[offscreen] Account registered in PXE and wallet:', address); + return { success: true, address, accountManager }; +} + +async function handleRegisterAccount(address: string, secret: string, salt: string) { + return registerAccountInWallet(address, secret, salt); +} + +/** + * Exports the entire wallet: decrypts all account secrets and builds a WalletExportData object. + */ +async function handleExportWallet(): Promise { + const masterKey = getCachedMasterKey(); + const allAccounts = await getAccounts(); + const activeAddr = await getActiveAccount(); + + reportProgress(`Decrypting ${allAccounts.length} account(s)...`); + + const exportedAccounts: WalletExportData['accounts'] = []; + for (const account of allAccounts) { + const secretData = await getAccountSecret(account.address, masterKey); + if (!secretData) { + throw new Error(`Failed to decrypt account: ${account.address}`); + } + exportedAccounts.push({ + address: account.address, + secret: secretData.secret, + salt: secretData.salt, + alias: account.alias, + isDeployed: account.isDeployed, + }); + } + + return { + version: 1, + aztecPackagesVersion: AZTEC_PACKAGES_VERSION, + exportedAt: new Date().toISOString(), + accounts: exportedAccounts, + activeAccount: activeAddr, + }; +} + +/** + * Imports accounts into the wallet: re-encrypts each account with the new master key, + * stores them, marks deployed ones, sets active account, and registers all with PXE. + */ +async function handleImportWalletAccounts( + accounts: WalletExportData['accounts'], + activeAccount: string | null, +): Promise<{ success: true }> { + const masterKey = getCachedMasterKey(); + + reportProgress(`Importing ${accounts.length} account(s)...`); + + for (const account of accounts) { + await storeAccount( + account.address, + account.secret, + account.salt, + masterKey, + account.alias, + ); + if (account.isDeployed) { + await markDeployed(account.address); + } + } + + if (activeAccount) { + await setActiveAccount(activeAccount); + } + + // Initialize PXE and register all accounts + reportProgress('Initializing PXE...'); + await ensurePXE(); + + for (const account of accounts) { + reportProgress(`Registering ${account.alias || account.address.slice(0, 10)}...`); + await registerAccountInWallet(account.address, account.secret, account.salt); + } + + reportProgress('Import complete!'); + return { success: true }; +} + +async function handleInitPXE(nodeUrl?: string) { + await ensurePXE(nodeUrl); + return { success: true }; +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx new file mode 100644 index 000000000000..ff08b6674353 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import type { StoredAccount } from './types'; +import { truncateAddress } from './helpers'; + +interface AccountSwitcherProps { + accounts: StoredAccount[]; + activeAccount: string | null; + onSelect: (address: string) => void; + onCreateNew: () => void; +} + +export function AccountSwitcher({ accounts, activeAccount, onSelect, onCreateNew }: AccountSwitcherProps) { + return ( +
+ {accounts.map((account) => { + const isActive = activeAccount === account.address; + return ( +
onSelect(account.address)} + > +
+
+ {account.alias || 'Unnamed Account'} +
+
+ {truncateAddress(account.address)} +
+
+
+ + {account.isDeployed ? 'Deployed' : 'Not Deployed'} + + {isActive && } +
+
+ ); + })} + + +
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx new file mode 100644 index 000000000000..51fdf367811a --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx @@ -0,0 +1,509 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { getOriginHost } from '../utils'; +import { hashToEmoji } from '@aztec/wallet-sdk/crypto'; +import type { PendingDiscovery } from '@aztec/wallet-sdk/extension/handlers'; +import type { PendingTransaction, PendingCapabilities, PendingSessionVerification } from '../shared-types'; +import { sendToBackground, truncateAddress } from './helpers'; + +interface ApprovalsViewProps { + discoveries: PendingDiscovery[]; + transactions: PendingTransaction[]; + pendingCapabilities: PendingCapabilities[]; + onRefresh: () => void; +} + +export function ApprovalsView({ + discoveries, + transactions, + pendingCapabilities, + onRefresh, +}: ApprovalsViewProps) { + const [processing, setProcessing] = useState(null); + const [error, setError] = useState(null); + + /** Generic approval/rejection handler — eliminates per-type boilerplate. */ + const handleAction = async (type: string, payload: Record, key: string) => { + try { + setProcessing(key); + setError(null); + await sendToBackground({ type, ...payload }); + } catch (err: any) { + // "Transaction not found" means it was already processed — just refresh + if (type !== MessageTypes.APPROVE_TRANSACTION) { + setError(err.message); + } else { + console.debug('Approve failed (likely stale):', err.message); + } + } finally { + setProcessing(null); + onRefresh(); + } + }; + + const handleApproveConnection = (requestId: string) => + handleAction(MessageTypes.APPROVE_CONNECTION, { requestId }, requestId); + const handleRejectConnection = (requestId: string) => + handleAction(MessageTypes.REJECT_CONNECTION, { requestId }, requestId); + const handleApproveTx = (messageId: string) => + handleAction(MessageTypes.APPROVE_TRANSACTION, { messageId }, messageId); + const handleRejectTx = (messageId: string) => + handleAction(MessageTypes.REJECT_TRANSACTION, { messageId }, messageId); + const handleApproveCapabilities = (messageId: string) => + handleAction(MessageTypes.APPROVE_CAPABILITIES, { messageId }, messageId); + const handleRejectCapabilities = (messageId: string) => + handleAction(MessageTypes.REJECT_CAPABILITIES, { messageId }, messageId); + + if (discoveries.length === 0 && transactions.length === 0 && pendingCapabilities.length === 0) { + return ( +
+
+
No pending approvals
+
+ ); + } + + return ( +
+ {error &&
{error}
} + + {discoveries.map((discovery) => ( + handleApproveConnection(discovery.requestId)} + onReject={() => handleRejectConnection(discovery.requestId)} + processing={processing === discovery.requestId} + /> + ))} + + {transactions.map((tx) => ( + handleApproveTx(tx.messageId)} + onReject={() => handleRejectTx(tx.messageId)} + processing={processing === tx.messageId} + /> + ))} + + {pendingCapabilities.map((cap) => ( + handleApproveCapabilities(cap.messageId)} + onReject={() => handleRejectCapabilities(cap.messageId)} + processing={processing === cap.messageId} + /> + ))} +
+ ); +} + +export function SessionVerificationView({ verification, onConfirm, onReject }: { + verification: PendingSessionVerification; + onConfirm: () => void; + onReject: () => void; +}) { + const emojis = hashToEmoji(verification.verificationHash); + + return ( +
+
+
+
🔐
+
+
{getOriginHost(verification.origin)}
+
Verify Connection
+
+
+ +
+

+ Confirm these emojis match what the dApp shows: +

+
{emojis}
+
+ +
+ + +
+
+
+ ); +} + +// docs:start:connection-approval +interface ConnectionApprovalProps { + discovery: PendingDiscovery; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function ConnectionApproval({ + discovery, + onApprove, + onReject, + processing, +}: ConnectionApprovalProps) { + return ( +
+
+
🔗
+
+
{getOriginHost(discovery.origin)}
+
Connection Request
+
+
+ +
+
+ Origin + {discovery.origin} +
+ {discovery.appId && ( +
+ App ID + {discovery.appId} +
+ )} +
+ +
+ + +
+
+ ); +} +// docs:end:connection-approval + +// docs:start:transaction-approval +interface TransactionApprovalProps { + transaction: PendingTransaction; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function TransactionApproval({ + transaction, + onApprove, + onReject, + processing, +}: TransactionApprovalProps) { + const methodLabels: Record = { + sendTx: 'Send Transaction', + simulateTx: 'Simulate Transaction', + createAuthWit: 'Create Authorization', + profileTx: 'Profile Transaction', + batch: 'Batch Transaction', + }; + + return ( +
+
+
📝
+
+
{getOriginHost(transaction.origin)}
+
+ {methodLabels[transaction.method] || transaction.method} +
+
+
+ +
+
+ From + {truncateAddress(transaction.from)} +
+
+ Method + {transaction.method} +
+ + {/* sendTx: show function calls from the execution payload (args[0]) */} + {transaction.method === 'sendTx' && transaction.args?.[0]?.calls && ( +
+
Function Calls:
+ {transaction.args[0].calls.map((call: any, i: number) => ( +
+
{call.name || 'Unknown Function'}
+
Contract: {truncateAddress(call.to?.toString?.() || '')}
+
+ ))} +
+ )} + + {/* batch: show list of batched operations and their function calls */} + {transaction.method === 'batch' && Array.isArray(transaction.args?.[0]) && ( +
+
Batched Operations:
+ {transaction.args[0].map((method: any, i: number) => ( +
+
+ {methodLabels[method.name] || method.name} +
+ {method.name === 'sendTx' && method.args?.[0]?.calls?.map((call: any, j: number) => ( +
+ {call.name || 'Unknown'} → {truncateAddress(call.to?.toString?.() || '')} +
+ ))} +
+ ))} +
+ )} +
+ +
+ + +
+
+ ); +} +// docs:end:transaction-approval + +interface ExpandableSection { + summary: string; + items: string[]; +} + +interface CapabilityDescription { + label: string; + details: string[]; + expandable: ExpandableSection[]; +} + +function formatPatterns(scope: any): { summary: string; items: string[] } { + if (!Array.isArray(scope)) return { summary: 'unknown', items: [] }; + const items = scope.map((p: any) => { + const contract = p.contract === '*' ? '*' : truncateAddress(String(p.contract)); + const fn = p.function || '*'; + return `${contract}:${fn}`; + }); + return { summary: `${scope.length} specific pattern(s)`, items }; +} + +function describeCapability(cap: any): CapabilityDescription { + switch (cap.type) { + case 'accounts': { + const details = []; + if (cap.canGet) details.push('View account addresses'); + if (cap.canCreateAuthWit) details.push('Create authentication witnesses'); + return { label: 'Account Access', details: details.length ? details : ['Basic account access'], expandable: [] }; + } + case 'contracts': { + const expandable: ExpandableSection[] = []; + let scopeText: string; + if (cap.contracts === '*') { + scopeText = 'Scope: All contracts'; + } else if (Array.isArray(cap.contracts)) { + scopeText = `Scope: ${cap.contracts.length} specific contract(s)`; + expandable.push({ + summary: `${cap.contracts.length} specific contract(s)`, + items: cap.contracts.map((c: any) => truncateAddress(String(c))), + }); + } else { + scopeText = 'Scope: unknown'; + } + return { + label: 'Contract Access', + details: [ + cap.canRegister ? 'Register contracts' : '', + cap.canGetMetadata ? 'Query contract metadata' : '', + scopeText, + ].filter(Boolean), + expandable, + }; + } + case 'transaction': { + const expandable: ExpandableSection[] = []; + let scopeText: string; + if (cap.scope === '*') { + scopeText = 'Scope: Any transaction'; + } else if (Array.isArray(cap.scope)) { + const { summary, items } = formatPatterns(cap.scope); + scopeText = `Scope: ${summary}`; + expandable.push({ summary, items }); + } else { + scopeText = 'Scope: unknown'; + } + return { label: 'Send Transactions', details: [scopeText], expandable }; + } + case 'simulation': { + const expandable: ExpandableSection[] = []; + const details: string[] = []; + if (cap.transactions) { + if (cap.transactions.scope === '*') { + details.push('Tx simulation: any'); + } else if (Array.isArray(cap.transactions.scope)) { + const { summary, items } = formatPatterns(cap.transactions.scope); + details.push(`Tx simulation: ${summary}`); + expandable.push({ summary: `Tx: ${summary}`, items }); + } + } + if (cap.utilities) { + if (cap.utilities.scope === '*') { + details.push('Utility calls: any'); + } else if (Array.isArray(cap.utilities.scope)) { + const { summary, items } = formatPatterns(cap.utilities.scope); + details.push(`Utility calls: ${summary}`); + expandable.push({ summary: `Util: ${summary}`, items }); + } + } + return { label: 'Simulate Transactions', details, expandable }; + } + case 'data': + return { + label: 'Data Access', + details: [ + cap.addressBook ? 'Address book' : '', + cap.privateEvents ? 'Private events' : '', + ].filter(Boolean), + expandable: [], + }; + default: + return { label: cap.type, details: ['Unknown capability'], expandable: [] }; + } +} + +interface CapabilitiesApprovalProps { + pending: PendingCapabilities; + onApprove: () => void; + onReject: () => void; + processing: boolean; +} + +function CapabilitiesApproval({ + pending, + onApprove, + onReject, + processing, +}: CapabilitiesApprovalProps) { + const [expanded, setExpanded] = useState>(new Set()); + + const toggleExpanded = (key: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( +
+
+
🔒
+
+
{getOriginHost(pending.origin)}
+
Capabilities Request
+
+
+ +
+
+ App + + {pending.appMetadata.name} v{pending.appMetadata.version} + +
+ {pending.appMetadata.description && ( +
+ Description + {pending.appMetadata.description} +
+ )} +
+ Origin + {pending.origin} +
+ +
+
Requested Permissions:
+ {pending.capabilities.map((cap, i) => { + const desc = describeCapability(cap); + return ( +
+
{desc.label}
+ {desc.details.map((detail, j) => ( +
{detail}
+ ))} + {desc.expandable.map((section, k) => { + const key = `${i}-${k}`; + const isExpanded = expanded.has(key); + return ( +
+ + {isExpanded && ( +
+ {section.items.map((item, l) => ( +
{item}
+ ))} +
+ )} +
+ ); + })} +
+ ); + })} +
+
+ +
+ + +
+
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx new file mode 100644 index 000000000000..2201124a29f8 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +import { createAndActivateAccount } from './helpers'; + +export function CreateAccountView({ onCreated }: { onCreated: () => void }) { + const [alias, setAlias] = useState(''); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + const handleCreate = async () => { + setCreating(true); + setError(null); + try { + await createAndActivateAccount(alias || 'My Account'); + onCreated(); + } catch (err: any) { + setError(err.message); + } finally { + setCreating(false); + } + }; + + return ( +
+ {error &&
{error}
} + +
+ + setAlias(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreate()} + placeholder="My Account" + disabled={creating} + /> +
+ + +
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx new file mode 100644 index 000000000000..610405811bb1 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import { getOriginHost } from '../utils'; +import type { ConnectedSite } from './types'; + +export function Header({ pendingCount, onApprovalClick, connectedSites, onDisconnect, onSettingsClick }: { + pendingCount: number; + onApprovalClick: () => void; + connectedSites: ConnectedSite[]; + onDisconnect: (sessionId: string) => void; + onSettingsClick: () => void; +}) { + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); + const connected = connectedSites.length > 0; + + // Close dropdown when clicking outside + useEffect(() => { + if (!showDropdown) return; + const handleClick = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowDropdown(false); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [showDropdown]); + + return ( +
+

Aztec Wallet

+
+ local + +
+ + + {showDropdown && connected && ( +
+ {connectedSites.map((site) => ( +
+ {getOriginHost(site.origin)} + +
+ ))} +
+ )} +
+ + + + +
+
+ ); +} + +export function SubHeader({ title, onBack }: { title: string; onBack: () => void }) { + return ( +
+ + {title} +
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx new file mode 100644 index 000000000000..16a4677133be --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { sendToBackground, waitForTask } from './helpers'; + +export function LockScreen({ onUnlocked }: { onUnlocked: () => void }) { + const [password, setPassword] = useState(''); + const [unlocking, setUnlocking] = useState(false); + const [error, setError] = useState(null); + + const handleUnlock = async () => { + if (!password) return; + setUnlocking(true); + setError(null); + try { + const { taskId } = await sendToBackground({ + type: MessageTypes.UNLOCK_WALLET, + password, + }); + await waitForTask(taskId); + onUnlocked(); + } catch (err: any) { + setError(err.message); + } finally { + setUnlocking(false); + } + }; + + return ( +
+
🔒
+

Wallet Locked

+

+ Enter your password to unlock. +

+ + {error &&
{error}
} + +
+ setPassword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleUnlock()} + placeholder="Enter password" + disabled={unlocking} + /> +
+ + +
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx new file mode 100644 index 000000000000..d6f8debe060b --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx @@ -0,0 +1,154 @@ +import React, { useState, useRef } from 'react'; + +import { MessageTypes, AZTEC_PACKAGES_VERSION } from '../config'; +import type { WalletExportData } from '../shared-types'; +import { sendToBackground, waitForTask } from './helpers'; + +export function SettingsPage({ onImportStart }: { onImportStart: (data: WalletExportData) => void }) { + const [exporting, setExporting] = useState(false); + const [importPreview, setImportPreview] = useState(null); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const handleExport = async () => { + setExporting(true); + setError(null); + try { + const { taskId } = await sendToBackground({ type: MessageTypes.EXPORT_WALLET }); + const result = await waitForTask(taskId); + + const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const date = new Date().toISOString().slice(0, 10); + const a = document.createElement('a'); + a.href = url; + a.download = `aztec-wallet-backup-${date}.json`; + a.click(); + URL.revokeObjectURL(url); + } catch (err: any) { + setError(err.message); + } finally { + setExporting(false); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setError(null); + setImportPreview(null); + + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const data = JSON.parse(ev.target?.result as string) as WalletExportData; + if (data.version !== 1) { + setError('Unsupported backup version'); + return; + } + if (!Array.isArray(data.accounts) || data.accounts.some((a) => !a.address || !a.secret || !a.salt)) { + setError('Invalid backup file: missing account data'); + return; + } + setImportPreview(data); + } catch { + setError('Failed to parse backup file'); + } + }; + reader.readAsText(file); + // Reset input so the same file can be selected again + e.target.value = ''; + }; + + const versionMismatch = importPreview && importPreview.aztecPackagesVersion !== AZTEC_PACKAGES_VERSION; + + return ( +
+ {error &&
{error}
} + + {/* Export section */} +
+
Export Wallet Backup
+
+ Download a JSON file containing all your accounts with decrypted secrets. + Store this file securely. +
+ +
+ + {/* Import section */} +
+
Import Wallet Backup
+
+ Restore accounts from a previously exported backup file. +
+ + + + + + {importPreview && ( +
+
Backup Preview
+
+ Accounts + {importPreview.accounts.length} +
+
+ Aztec Version + {importPreview.aztecPackagesVersion} +
+
+ Exported + {new Date(importPreview.exportedAt).toLocaleDateString()} +
+ + {versionMismatch && ( +
+ Version mismatch! This backup was created with {importPreview.aztecPackagesVersion} but + the current wallet uses {AZTEC_PACKAGES_VERSION}. Imported addresses may not match. +
+ )} + +
+ This will wipe your current wallet. You will need to set a new master password. +
+ + +
+ )} +
+ + {/* Version info */} +
+ Aztec Packages Version + {AZTEC_PACKAGES_VERSION} +
+
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx new file mode 100644 index 000000000000..9a27d42df459 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; + +import { MessageTypes } from '../config'; +import { sendToBackground, waitForTask, createAndActivateAccount } from './helpers'; + +export function SetupScreen({ onComplete, skipAccountCreation = false }: { onComplete: () => void; skipAccountCreation?: boolean }) { + const [step, setStep] = useState<'password' | 'account'>('password'); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [alias, setAlias] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSetPassword = async () => { + if (password.length < 8) { + setError('Password must be at least 8 characters'); + return; + } + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + setLoading(true); + setError(null); + try { + const { taskId } = await sendToBackground({ type: MessageTypes.SETUP_PASSWORD, password }); + await waitForTask(taskId); + if (skipAccountCreation) { + onComplete(); + return; + } + setStep('account'); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleCreateFirstAccount = async () => { + setLoading(true); + setError(null); + try { + await createAndActivateAccount(alias || 'Account 1'); + onComplete(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (step === 'password') { + return ( +
+
🔒
+

Welcome to Aztec Wallet

+

+ Set a master password to protect your accounts. +

+ + {error &&
{error}
} + +
+ setPassword(e.target.value)} + placeholder="Password (min 8 characters)" + disabled={loading} + /> +
+ +
+ setConfirmPassword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSetPassword()} + placeholder="Confirm password" + disabled={loading} + /> +
+ + +
+ ); + } + + return ( +
+
👤
+

Create Your First Account

+

+ Choose a name for your first Aztec account. +

+ + {error &&
{error}
} + +
+ setAlias(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCreateFirstAccount()} + placeholder="Account name (e.g. Account 1)" + disabled={loading} + /> +
+ + +
+ ); +} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts b/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts new file mode 100644 index 000000000000..252c5d3a22a4 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts @@ -0,0 +1,100 @@ +import { MessageTarget, MessageTypes } from '../config'; +import type { BackgroundTask } from '../shared-types'; + +// docs:start:send-message +/** + * Sends a message to the background script via chrome.runtime.sendMessage. + * Used for simple request/response calls (accounts, status, approvals). + */ +export function sendToBackground(message: any): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { ...message, target: MessageTarget.BACKGROUND }, + (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + return; + } + if (response?.success) { + resolve(response.result); + } else { + reject(new Error(response?.error || 'Unknown error')); + } + } + ); + }); +} +// docs:end:send-message + +/** + * Registry of pending task promises. (#12) + * + * When the background pushes a task-update via the port, we resolve/reject + * the corresponding promise. If the popup was closed and reopened, the initial + * state push includes all recent tasks — we process completed ones immediately + * so waitForTask resolves even if the task finished while we were closed. + */ +export const pendingTaskCallbacks = new Map void; + reject: (error: Error) => void; +}>(); + +export function waitForTask(taskId: string, timeoutMs = 300000): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + pendingTaskCallbacks.delete(taskId); + reject(new Error('Task timed out')); + }, timeoutMs); + + pendingTaskCallbacks.set(taskId, { + resolve: (value: any) => { + clearTimeout(timeoutId); + pendingTaskCallbacks.delete(taskId); + resolve(value); + }, + reject: (error: Error) => { + clearTimeout(timeoutId); + pendingTaskCallbacks.delete(taskId); + reject(error); + }, + }); + }); +} + +export function handleTaskUpdate(task: BackgroundTask) { + const callbacks = pendingTaskCallbacks.get(task.id); + if (!callbacks) return; + + if (task.status === 'success') { + callbacks.resolve(task.result); + } else if (task.status === 'error') { + callbacks.reject(new Error(task.error || 'Task failed')); + } +} + +// docs:start:helpers +export function truncateAddress(address: string): string { + if (!address) return ''; + if (address.length <= 16) return address; + return `${address.slice(0, 8)}...${address.slice(-6)}`; +} + +/** + * Creates an account and sets it as active. + * Shared between SetupScreen (first account) and CreateAccountView (additional accounts). + */ +export async function createAndActivateAccount(alias: string): Promise { + const { taskId } = await sendToBackground({ + type: MessageTypes.CREATE_ACCOUNT, + alias, + }); + const result = await waitForTask(taskId); + + if (result?.address) { + await sendToBackground({ + type: MessageTypes.SET_ACTIVE_ACCOUNT, + address: result.address, + }); + } +} +// docs:end:helpers diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx new file mode 100644 index 000000000000..f44a5151ceca --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx @@ -0,0 +1,467 @@ +/** + * Popup UI for the Aztec Tutorial Wallet. + * + * MetaMask-like layout with: + * - Setup screen (first-time password + first account) + * - Lock screen (unlock with master password) + * - Main page (active account detail, deploy button) + * - Account switcher overlay + * - Create account sub-page + * - Approvals view for connection/transaction requests + * + * Communication: + * - Persistent port to background for real-time push updates (no polling) + * - Port auto-reconnects if background disconnects (#9) + * - Initial state comes from the port, not separate fetches (#10) + */ + +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { MessageTypes } from '../config'; +import type { WalletExportData, PublicAccountInfo, PendingTransaction, PendingCapabilities, ConnectedSite, PendingSessionVerification, BackgroundTask, View } from '../shared-types'; +import type { PendingDiscovery } from '@aztec/wallet-sdk/extension/handlers'; +import { sendToBackground, waitForTask, handleTaskUpdate } from './helpers'; +import { Header, SubHeader } from './Header'; +import { SetupScreen } from './SetupScreen'; +import { LockScreen } from './LockScreen'; +import { MainPage } from './MainScreen'; +import { AccountSwitcher } from './AccountSwitcher'; +import { CreateAccountView } from './CreateAccountView'; +import { ApprovalsView, SessionVerificationView } from './ApprovalView'; +import { SettingsPage } from './SettingsPage'; + +// docs:start:main-app +function App() { + const [view, setView] = useState('loading'); + const [accounts, setAccounts] = useState([]); + const [activeAccount, setActiveAccount] = useState(null); + const [discoveries, setDiscoveries] = useState([]); + const [transactions, setTransactions] = useState([]); + const [connectedSites, setConnectedSites] = useState([]); + const [sessionVerifications, setSessionVerifications] = useState([]); + const [pendingCapabilities, setPendingCapabilities] = useState([]); + const [runningTasks, setRunningTasks] = useState([]); + const [error, setError] = useState(null); + const [elapsed, setElapsed] = useState(0); + const [pendingImportData, setPendingImportData] = useState(null); + const portRef = useRef(null); + const reconnectTimerRef = useRef | null>(null); + + const pendingCount = discoveries.length + transactions.length + sessionVerifications.length + pendingCapabilities.length; + + /** + * Applies state pushed from the background via the port. (#10) + * This is the single source of truth for pending items, tasks, and connected sites. + */ + const applyBackgroundState = useCallback((data: any) => { + if (data.discoveries) setDiscoveries(data.discoveries); + if (data.transactions) setTransactions(data.transactions); + if (data.pendingSessionVerifications) setSessionVerifications(data.pendingSessionVerifications); + if (data.pendingCapabilities) setPendingCapabilities(data.pendingCapabilities); + if (data.connectedSites) setConnectedSites(data.connectedSites); + if (data.tasks) { + setRunningTasks(data.tasks.filter((t: BackgroundTask) => t.status === 'running')); + // Resolve any waitForTask promises for completed tasks (#12) + for (const task of data.tasks) { + handleTaskUpdate(task); + } + } + }, []); + + /** + * Loads account data and determines the initial view. + * Pending items come from the port push, NOT from a separate fetch. (#10) + */ + const loadData = useCallback(async () => { + try { + setError(null); + + const [accountsResult, activeAccountResult, statusResult] = await Promise.all([ + sendToBackground({ type: MessageTypes.GET_ACCOUNTS }), + sendToBackground({ type: MessageTypes.GET_ACTIVE_ACCOUNT }), + sendToBackground({ type: MessageTypes.GET_WALLET_STATUS }), + ]); + + setAccounts(accountsResult || []); + setActiveAccount(activeAccountResult || null); + + const unlocked = statusResult?.unlocked || false; + const hasPassword = statusResult?.hasPassword || false; + const hasAccounts = (accountsResult || []).length > 0; + + if (!hasPassword && !hasAccounts) { + setView('setup'); + } else if (!unlocked) { + setView('lock'); + } else { + setView((prev) => prev === 'loading' ? 'main' : prev); + } + } catch (err: any) { + console.error('Failed to load data:', err); + setError(err.message); + setView('setup'); + } + }, []); + + /** + * Connect persistent port to background. (#9) + * Reconnects automatically if the background disconnects (e.g., SW restart). + */ + const connectPort = useCallback(() => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + + try { + const port = chrome.runtime.connect({ name: 'popup' }); + portRef.current = port; + + port.onMessage.addListener((message: any) => { + if (message.type === 'state') { + applyBackgroundState(message.data); + + // Auto-navigate to approvals/verification if there are pending items + const d = message.data.discoveries?.length || 0; + const t = message.data.transactions?.length || 0; + const sv = message.data.pendingSessionVerifications?.length || 0; + const c = message.data.pendingCapabilities?.length || 0; + if (sv > 0) { + setView((prev) => (prev === 'main' || prev === 'loading' || prev === 'approvals') ? 'verifySession' : prev); + } else if (d > 0 || t > 0 || c > 0) { + setView((prev) => (prev === 'main' || prev === 'loading') ? 'approvals' : prev); + } + } else if (message.type === 'task-update') { + const task: BackgroundTask = message.task; + handleTaskUpdate(task); + + setRunningTasks((prev) => { + if (task.status === 'running') { + const existing = prev.findIndex((t) => t.id === task.id); + if (existing >= 0) { + const updated = [...prev]; + updated[existing] = task; + return updated; + } + return [...prev, task]; + } + return prev.filter((t) => t.id !== task.id); + }); + + // Refresh account data if a state-changing task completed + if (task.status === 'success') { + const refreshTypes = ['create-account', 'deploy-account', 'unlock', 'setup-password', 'import-wallet-accounts']; + if (refreshTypes.includes(task.type)) { + loadData(); + } + } + } + }); + + port.onDisconnect.addListener(() => { + console.log('[popup] Port disconnected, will reconnect...'); + portRef.current = null; + // Reconnect after a short delay (SW may be restarting) (#9) + reconnectTimerRef.current = setTimeout(connectPort, 1000); + }); + + } catch (err) { + console.error('[popup] Failed to connect port:', err); + // Retry connection (#9) + reconnectTimerRef.current = setTimeout(connectPort, 2000); + } + }, [applyBackgroundState, loadData]); + + useEffect(() => { + connectPort(); + loadData(); + + return () => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + } + if (portRef.current) { + portRef.current.disconnect(); + portRef.current = null; + } + }; + }, [connectPort, loadData]); + + // Reactive auto-navigation: ensures the popup shows the right view whenever + // pending items exist, even if the port message handler's auto-nav fired while + // the popup was on a non-target view (e.g. 'setup' or 'lock'). + useEffect(() => { + if (sessionVerifications.length > 0 && + (view === 'main' || view === 'loading' || view === 'approvals')) { + setView('verifySession'); + } else if ((discoveries.length > 0 || transactions.length > 0 || pendingCapabilities.length > 0) && + (view === 'main' || view === 'loading')) { + setView('approvals'); + } + }, [sessionVerifications, discoveries, transactions, pendingCapabilities, view]); + + // Tick elapsed time while tasks are running + useEffect(() => { + if (runningTasks.length === 0) { + setElapsed(0); + return; + } + const oldest = Math.min(...runningTasks.map((t) => t.startedAt)); + setElapsed(Math.round((Date.now() - oldest) / 1000)); + const timer = setInterval(() => { + setElapsed(Math.round((Date.now() - oldest) / 1000)); + }, 1000); + return () => clearInterval(timer); + }, [runningTasks]); + + const handleUnlocked = () => { + setView('main'); + loadData(); + }; + + const handleSetupComplete = async () => { + if (pendingImportData) { + try { + const { taskId } = await sendToBackground({ + type: MessageTypes.IMPORT_WALLET_ACCOUNTS, + accounts: pendingImportData.accounts, + activeAccount: pendingImportData.activeAccount, + }); + await waitForTask(taskId); + setPendingImportData(null); + } catch (err: any) { + console.error('Failed to import accounts:', err); + setError(err.message); + setPendingImportData(null); + } + } + setView('main'); + loadData(); + }; + + const handleImportStart = (data: WalletExportData) => { + setPendingImportData(data); + sendToBackground({ type: MessageTypes.IMPORT_WALLET }).then(() => { + setView('setup'); + }).catch((err) => { + console.error('Failed to wipe wallet:', err); + setError(err.message); + setPendingImportData(null); + }); + }; + + const handleDisconnectSite = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.DISCONNECT_SESSION, sessionId }); + } catch (err) { + console.error('Failed to disconnect session:', err); + } + }; + + const handleConfirmSession = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.CONFIRM_SESSION, sessionId }); + setView('main'); + } catch (err) { + console.error('Failed to confirm session:', err); + } + }; + + const handleRejectSession = async (sessionId: string) => { + try { + await sendToBackground({ type: MessageTypes.REJECT_SESSION, sessionId }); + setView('main'); + } catch (err) { + console.error('Failed to reject session:', err); + } + }; + + const activeAccountData = accounts.find((a) => a.address === activeAccount) || accounts[0] || null; + + const handleApprovalClick = () => { + if (sessionVerifications.length > 0) { + setView('verifySession'); + } else { + setView('approvals'); + } + }; + + const noopDisconnect = () => {}; + + if (view === 'loading') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> +
+
+ Loading... +
+
+ ); + } + + if (view === 'setup') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> + +
+ ); + } + + if (view === 'lock') { + return ( +
+
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> + +
+ ); + } + + if (view === 'approvals') { + return ( +
+
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> + setView('main')} /> + +
+ ); + } + + if (view === 'verifySession') { + const currentVerification = sessionVerifications[0]; + return ( +
+
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> + setView('main')} /> + {currentVerification ? ( + handleConfirmSession(currentVerification.sessionId)} + onReject={() => handleRejectSession(currentVerification.sessionId)} + /> + ) : ( +
+
+
No pending verifications
+
+ )} +
+ ); + } + + if (view === 'switcher') { + return ( +
+
setView('settings')} /> + setView('main')} /> + { + sendToBackground({ type: MessageTypes.SET_ACTIVE_ACCOUNT, address }) + .then(() => { setActiveAccount(address); setView('main'); }) + .catch((err) => console.error('Failed to switch account:', err)); + }} + onCreateNew={() => setView('createAccount')} + /> +
+ ); + } + + if (view === 'createAccount') { + return ( +
+
setView('settings')} /> + setView('switcher')} /> + { setView('main'); loadData(); }} /> +
+ ); + } + + if (view === 'settings') { + return ( +
+
{}} /> + setView('main')} /> + +
+ ); + } + + // Main view + return ( +
+
setView('settings')} /> + + {error &&
{error}
} + + {runningTasks.length > 0 && ( +
+
+
+ {runningTasks.map((t) => { + const labels: Record = { + 'deploy-account': 'Deploying account...', + 'create-account': 'Creating account...', + 'unlock': 'Unlocking wallet...', + 'setup-password': 'Setting up password...', + 'export-wallet': 'Exporting wallet...', + 'import-wallet-accounts': 'Importing accounts...', + }; + const genericLabel = t.type.startsWith('wallet:') + ? `Processing ${t.type.replace('wallet:', '')}...` + : t.type.startsWith('tx:') + ? `Executing ${t.type.replace('tx:', '')}...` + : labels[t.type] || 'Processing...'; + return ( +
+
{t.progress || genericLabel}
+ {t.progress && ( +
{genericLabel}
+ )} +
+ ); + })} +
+ Elapsed: {Math.floor(elapsed / 60)}:{String(elapsed % 60).padStart(2, '0')} +
+
+
+ )} + + {activeAccountData ? ( + 0} + onSwitcherOpen={() => setView('switcher')} + onRefresh={loadData} + /> + ) : ( +
+
👛
+
No accounts yet
+ +
+ )} +
+ ); +} +// docs:end:main-app + +// Mount the app +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts b/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts new file mode 100644 index 000000000000..16e1e8f4ab46 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts @@ -0,0 +1,18 @@ +/** + * Re-export shared types for the popup. + * The canonical definitions live in shared-types.ts — import from there + * or from this module (which re-exports everything the popup needs). + */ +export type { + PublicAccountInfo, + PendingTransaction, + PendingSessionVerification, + PendingCapabilities, + ConnectedSite, + BackgroundTask, + WalletExportData, + View, +} from '../shared-types'; + +/** Alias for popup components that display account info. */ +export type { PublicAccountInfo as StoredAccount } from '../shared-types'; diff --git a/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts b/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts new file mode 100644 index 000000000000..4f8cc4c4448a --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts @@ -0,0 +1,97 @@ +/** + * Shared type definitions for the Aztec Tutorial Wallet extension. + * Single source of truth for types used across background, offscreen, and popup. + */ + +/** + * Public account info — the subset of account data safe to expose to the UI. + * Does NOT include encrypted secrets, IVs, or contract salts. + */ +export interface PublicAccountInfo { + address: string; + alias: string; + isDeployed: boolean; +} + +/** + * Pending transaction awaiting user approval. + */ +export interface PendingTransaction { + sessionId: string; + messageId: string; + method: string; + args: any; + from: string; + origin: string; + timestamp: number; +} + +/** + * Session pending emoji verification. + * After key exchange completes, we hold the session here until + * the user confirms the verification emojis match. + */ +export interface PendingSessionVerification { + sessionId: string; + origin: string; + appId: string; + verificationHash: string; + timestamp: number; +} + +/** + * Pending capability request awaiting user approval. + */ +export interface PendingCapabilities { + sessionId: string; + messageId: string; + origin: string; + /** App metadata from the capability manifest */ + appMetadata: { + name: string; + version: string; + description?: string; + url?: string; + icon?: string; + }; + /** Raw requested capabilities array */ + capabilities: Array<{ type: string; [key: string]: any }>; + timestamp: number; +} + +/** Connected site info for display in the popup. */ +export interface ConnectedSite { + sessionId: string; + origin: string; + appId: string; + connectedAt: number; +} + +/** Background task for long-running operations. */ +export interface BackgroundTask { + id: string; + type: string; + status: 'running' | 'success' | 'error'; + progress?: string; + result?: any; + error?: string; + startedAt: number; +} + +/** Wallet export data format for backup/restore. */ +export interface WalletExportData { + version: 1; + aztecPackagesVersion: string; + exportedAt: string; + accounts: Array<{ + address: string; + secret: string; + salt: string; + alias: string; + isDeployed: boolean; + }>; + activeAccount: string | null; +} + +/** Popup view state. */ +export type View = 'loading' | 'setup' | 'lock' | 'main' | 'switcher' | 'createAccount' | 'approvals' | 'verifySession' | 'settings'; diff --git a/docs/examples/webapp-tutorial/test-extension/src/utils.ts b/docs/examples/webapp-tutorial/test-extension/src/utils.ts new file mode 100644 index 000000000000..a6d5d42a4b00 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/utils.ts @@ -0,0 +1,44 @@ +/** + * Shared utilities for the Aztec Tutorial Wallet extension. + * Extracted from multiple files to eliminate duplication. + */ + +/** + * Resolves the real Chrome runtime object. + * Needed in offscreen documents where polyfills may shadow the global. + */ +export function getChromeRuntime(): typeof chrome { + const candidates = [ + typeof self !== 'undefined' ? (self as any).chrome : undefined, + typeof window !== 'undefined' ? (window as any).chrome : undefined, + (globalThis as any).chrome, + ]; + + for (const candidate of candidates) { + if (candidate?.runtime?.sendMessage) { + return candidate; + } + } + + throw new Error('Chrome runtime API not available'); +} + +/** + * Extracts a human-readable error message from any thrown value. + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +/** + * Extracts the hostname from an origin URL, falling back to the raw string. + */ +export function getOriginHost(origin: string): string { + try { + return new URL(origin).host; + } catch { + return origin; + } +} + diff --git a/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts b/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts new file mode 100644 index 000000000000..703afdc13832 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts @@ -0,0 +1,333 @@ +/** + * Encrypted key storage for the Aztec Tutorial Wallet. + * + * Security design: + * - Master password is NEVER stored or cached as a string (#2) + * - Password verification uses PBKDF2 + AES-GCM (encrypt known plaintext) (#1) + * - A non-extractable CryptoKey is derived once at unlock and cached in memory + * - All account secrets are encrypted with this master CryptoKey + * - Each account gets a unique random IV for AES-GCM + * + * WARNING: This is for tutorial purposes. Production wallets should use + * hardware security modules, secure enclaves, or platform keychain APIs. + */ + +/** Known plaintext used to verify the master password is correct */ +const VERIFICATION_PLAINTEXT = 'aztec-wallet-verify'; + +/** + * PBKDF2 iteration count. + * OWASP 2023 recommends >= 600,000 for SHA-256. + * Higher = slower brute force, but also slower unlock. + */ +const PBKDF2_ITERATIONS = 600_000; + +/** Stored account data structure */ +export interface StoredAccount { + /** Aztec address as hex string */ + address: string; + /** Encrypted secret key (base64 encoded) */ + encryptedSecret: string; + /** IV for AES-GCM (base64 encoded) — unique per account */ + iv: string; + /** User-friendly alias */ + alias: string; + /** Whether the account contract is deployed */ + isDeployed: boolean; + /** Account contract salt as hex string */ + contractSalt: string; +} + +/** Storage keys — shared with background.ts for direct chrome.storage access. */ +export const STORAGE_KEYS = { + ACCOUNTS: 'aztec_accounts', + PASSWORD_DATA: 'aztec_password_data', + ACTIVE_ACCOUNT: 'aztec_active_account', +} as const; + +/** Encode bytes to base64 */ +function bytesToBase64(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)); +} + +/** Decode base64 to bytes */ +function base64ToBytes(b64: string): Uint8Array { + return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); +} + +// docs:start:derive-master-key +/** + * Derives a non-extractable AES-GCM CryptoKey from a password and salt using PBKDF2. + * + * The key is non-extractable: once created, the raw key material cannot be read + * from JavaScript. This means even if an attacker has a reference to the CryptoKey + * object, they cannot extract the underlying bytes. + */ +export async function deriveMasterKey( + password: string, + salt: Uint8Array +): Promise { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: PBKDF2_ITERATIONS, + hash: 'SHA-256', + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, // non-extractable + ['encrypt', 'decrypt'] + ); +} +// docs:end:derive-master-key + +// docs:start:encrypt-decrypt +/** + * Encrypts a secret using a CryptoKey. + * Each call generates a fresh random IV for AES-GCM. + */ +export async function encryptWithKey( + secret: string, + key: CryptoKey +): Promise<{ encrypted: string; iv: string }> { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encoder = new TextEncoder(); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoder.encode(secret) + ); + + return { + encrypted: bytesToBase64(new Uint8Array(ciphertext)), + iv: bytesToBase64(iv), + }; +} + +/** + * Decrypts a secret using a CryptoKey. + */ +export async function decryptWithKey( + encrypted: string, + iv: string, + key: CryptoKey +): Promise { + const ivBytes = base64ToBytes(iv); + const ciphertextBytes = base64ToBytes(encrypted); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: ivBytes }, + key, + ciphertextBytes + ); + + return new TextDecoder().decode(decrypted); +} +// docs:end:encrypt-decrypt + +import { getChromeRuntime } from '../utils'; + +/** + * Chrome runtime for messaging (available in offscreen documents). + * Offscreen documents don't have direct chrome.storage access, + * so all storage operations are proxied through the background script. + */ + +/** + * Storage operations are proxied through the background script because + * offscreen documents have limited chrome API access (no chrome.storage). + */ +const STORAGE_PROXY_TIMEOUT_MS = 30_000; // 30 seconds + +async function storageGet(key: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); + getChromeRuntime().runtime.sendMessage( + { type: 'storage-get', key }, + (response: any) => { + clearTimeout(timer); + if (response?.success) { + resolve(response.result); + } else { + reject(new Error(response?.error || 'Storage get failed')); + } + } + ); + }); +} + +async function storageSet(data: Record): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); + getChromeRuntime().runtime.sendMessage( + { type: 'storage-set', data }, + (response: any) => { + clearTimeout(timer); + if (response?.success) { + resolve(); + } else { + reject(new Error(response?.error || 'Storage set failed')); + } + } + ); + }); +} + +// docs:start:password-management +/** + * Sets the master password for the first time. (#1) + * + * Instead of storing an unsalted SHA-256 hash (vulnerable to rainbow tables), + * we derive a CryptoKey via PBKDF2 with a random salt, then encrypt a known + * plaintext. Verification = re-derive key + try to decrypt. + * + * Returns the derived master CryptoKey so the caller can cache it immediately. + */ +export async function setupPassword(password: string): Promise { + const salt = crypto.getRandomValues(new Uint8Array(32)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const masterKey = await deriveMasterKey(password, salt); + + const encoder = new TextEncoder(); + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + masterKey, + encoder.encode(VERIFICATION_PLAINTEXT) + ); + + await storageSet({ + [STORAGE_KEYS.PASSWORD_DATA]: { + salt: bytesToBase64(salt), + iv: bytesToBase64(iv), + verifier: bytesToBase64(new Uint8Array(ciphertext)), + }, + }); + + return masterKey; +} + +/** + * Verifies the password and returns the derived master CryptoKey. (#1, #2) + * + * If the password is correct, returns the non-extractable CryptoKey. + * If wrong, returns null (AES-GCM decryption fails with wrong key). + * The caller should cache the CryptoKey and discard the password string. + */ +export async function verifyAndDeriveMasterKey(password: string): Promise { + const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); + if (!data) return null; + + const salt = base64ToBytes(data.salt); + const iv = base64ToBytes(data.iv); + const verifier = base64ToBytes(data.verifier); + + const masterKey = await deriveMasterKey(password, salt); + + try { + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + masterKey, + verifier + ); + const decoded = new TextDecoder().decode(decrypted); + if (decoded === VERIFICATION_PLAINTEXT) { + return masterKey; + } + return null; + } catch { + // AES-GCM decryption throws on wrong key (authentication tag mismatch) + return null; + } +} + +/** + * Checks if a master password has been set. + */ +export async function hasPassword(): Promise { + const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); + return !!data; +} +// docs:end:password-management + +// docs:start:account-operations +/** + * Saves an account to storage. + */ +export async function saveAccount(account: StoredAccount): Promise { + const accounts = await getStoredAccounts(); + const existingIndex = accounts.findIndex((a) => a.address === account.address); + + if (existingIndex >= 0) { + accounts[existingIndex] = account; + } else { + accounts.push(account); + } + + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); +} + +/** + * Retrieves all stored accounts. + */ +export async function getStoredAccounts(): Promise { + const result = await storageGet(STORAGE_KEYS.ACCOUNTS); + return result || []; +} + +/** + * Gets a specific account by address. + */ +export async function getStoredAccount( + address: string +): Promise { + const accounts = await getStoredAccounts(); + return accounts.find((a) => a.address === address); +} + +/** + * Updates an account's deployment status. + */ +export async function markAccountDeployed(address: string): Promise { + const accounts = await getStoredAccounts(); + const account = accounts.find((a) => a.address === address); + if (account) { + account.isDeployed = true; + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); + } +} + +/** + * Removes an account from storage. + */ +export async function removeAccount(address: string): Promise { + const accounts = await getStoredAccounts(); + const filtered = accounts.filter((a) => a.address !== address); + await storageSet({ [STORAGE_KEYS.ACCOUNTS]: filtered }); +} + +/** + * Gets the active account address. + */ +export async function getActiveAccount(): Promise { + const result = await storageGet(STORAGE_KEYS.ACTIVE_ACCOUNT); + return result || null; +} + +/** + * Sets the active account address. + */ +export async function setActiveAccount(address: string): Promise { + await storageSet({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: address }); +} +// docs:end:account-operations diff --git a/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts b/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts new file mode 100644 index 000000000000..2d1bd3637d16 --- /dev/null +++ b/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts @@ -0,0 +1,153 @@ +/** + * Wallet implementation for the Aztec Tutorial Wallet extension. + * + * With the crossOriginIsolated monkey-patch, Barretenberg WASM works in Chrome + * extension offscreen documents. This allows full cryptographic operations: + * - Fr.random() for secure random field elements + * - deriveKeys() and deriveSigningKey() for key derivation + * - Address computation with SchnorrAccountContract + * + * All encryption uses a non-extractable CryptoKey — the raw password + * is never stored or passed around after initial derivation. (#2) + */ + +import { Fr } from '@aztec/aztec.js/fields'; + +import { instantiateAccount } from '../account-utils'; +import type { PublicAccountInfo } from '../shared-types'; +import { + type StoredAccount, + getStoredAccounts, + getStoredAccount, + saveAccount, + encryptWithKey, + decryptWithKey, + markAccountDeployed, + getActiveAccount, + setActiveAccount, +} from './storage'; +import { log } from '../config'; + +/** + * Generates a new secret and salt for account creation using real Aztec primitives. + * Uses Fr.random() which is cryptographically secure via Barretenberg. + */ +export function generateSecret(): { secret: string; salt: string } { + log.debug('[wallet-manager] Generating new secret and salt with Fr.random()...'); + const secret = Fr.random().toString(); + const salt = Fr.random().toString(); + return { secret, salt }; +} + +/** + * Computes the account address from a secret and salt. + * Runs in the extension offscreen document where Barretenberg works. + */ +export async function computeAddress(secretHex: string, saltHex: string): Promise { + log.debug('[wallet-manager] Computing address...'); + const { instance } = await instantiateAccount(secretHex, saltHex); + log.debug('[wallet-manager] Computed address:', instance.address.toString()); + return instance.address.toString(); +} + +/** + * Creates a complete account: generates secret/salt, computes address, and stores encrypted. + * Takes a CryptoKey (not a password string) for encryption. (#2) + */ +// docs:start:create-new-account +export async function createAccount( + masterKey: CryptoKey, + alias: string = '' +): Promise<{ address: string; secret: string; salt: string }> { + log.debug('[wallet-manager] Creating account...'); + + const { secret, salt } = generateSecret(); + const address = await computeAddress(secret, salt); + await storeAccount(address, secret, salt, masterKey, alias); + + // Auto-set as active if this is the first account + const currentActive = await getActiveAccount(); + if (!currentActive) { + await setActiveAccount(address); + log.debug('[wallet-manager] Set as active account (first account)'); + } + + log.debug('[wallet-manager] Account created:', address); + return { address, secret, salt }; +} +// docs:end:create-new-account + +/** + * Stores an account with encrypted secret. + * Uses the master CryptoKey for AES-GCM encryption with a per-account random IV. + */ +export async function storeAccount( + address: string, + secret: string, + salt: string, + masterKey: CryptoKey, + alias: string = '' +): Promise { + log.debug('[wallet-manager] Storing account:', address); + + const { encrypted, iv } = await encryptWithKey(secret, masterKey); + + const storedAccount: StoredAccount = { + address, + encryptedSecret: encrypted, + iv, + alias, + isDeployed: false, + contractSalt: salt, + }; + + await saveAccount(storedAccount); + log.debug('[wallet-manager] Account stored successfully'); +} + +/** + * Gets the decrypted secret for an account. + * Takes a CryptoKey (not a password string). (#2) + */ +export async function getAccountSecret( + address: string, + masterKey: CryptoKey +): Promise<{ secret: string; salt: string } | null> { + const stored = await getStoredAccount(address); + if (!stored) { + return null; + } + + const secret = await decryptWithKey( + stored.encryptedSecret, + stored.iv, + masterKey + ); + + return { + secret, + salt: stored.contractSalt, + }; +} + +/** + * Gets all stored accounts (without secrets). + */ +export async function getAccounts(): Promise { + const accounts = await getStoredAccounts(); + return accounts.map(acc => ({ + address: acc.address, + alias: acc.alias, + isDeployed: acc.isDeployed, + })); +} + +/** + * Marks an account as deployed. + */ +export async function markDeployed(address: string): Promise { + await markAccountDeployed(address); +} + +// Re-export storage functions for convenience +export { getActiveAccount, setActiveAccount }; diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 201f01f37175..170a0da3e386 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -79,7 +79,6 @@ contract TokenPortal { return (key, index); } - // docs:start:deposit_private /** * @notice Deposit funds into the portal and adds an L2 message which can only be consumed privately on Aztec * @param _amount - The amount to deposit @@ -90,7 +89,6 @@ contract TokenPortal { function depositToAztecPrivate(uint256 _amount, bytes32 _secretHashForL2MessageConsumption) external returns (bytes32, uint256) - // docs:end:deposit_private { // Preamble DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index 50d48009eb58..91d676a90e09 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -9,7 +9,6 @@ import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {DataStructures as PortalDataStructures} from "./DataStructures.sol"; import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; -// docs:start:setup import {TokenPortal} from "./TokenPortal.sol"; import {ISwapRouter} from "../external/ISwapRouter.sol"; @@ -46,9 +45,6 @@ contract UniswapPortal { bytes32 contentHash; } - // docs:end:setup - - // docs:start:solidity_uniswap_swap_public /** * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again publicly * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. @@ -154,9 +150,6 @@ contract UniswapPortal { return TokenPortal(_outputTokenPortal).depositToAztecPublic(_aztecRecipient, amountOut, _secretHashForL1ToL2Message); } - // docs:end:solidity_uniswap_swap_public - - // docs:start:solidity_uniswap_swap_private /** * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again privately * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. @@ -258,4 +251,3 @@ contract UniswapPortal { return TokenPortal(_outputTokenPortal).depositToAztecPrivate(amountOut, _secretHashForL1ToL2Message); } } -// docs:end:solidity_uniswap_swap_private diff --git a/noir-projects/aztec-nr/aztec/src/authwit/account.nr b/noir-projects/aztec-nr/aztec/src/authwit/account.nr index aa07575d888a..e35a5378ac05 100644 --- a/noir-projects/aztec-nr/aztec/src/authwit/account.nr +++ b/noir-projects/aztec-nr/aztec/src/authwit/account.nr @@ -52,7 +52,6 @@ impl AccountActions<&mut PrivateContext> { /// transaction to be sent with a higher priority fee. This can be used to cancel the first transaction sent, /// assuming it hasn't been mined yet. /// - // docs:start:entrypoint pub fn entrypoint(self, app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { let valid_fn = self.is_valid_impl; diff --git a/noir-projects/aztec-nr/aztec/src/authwit/entrypoint/app.nr b/noir-projects/aztec-nr/aztec/src/authwit/entrypoint/app.nr index 8064be5148d9..9af791d5b148 100644 --- a/noir-projects/aztec-nr/aztec/src/authwit/entrypoint/app.nr +++ b/noir-projects/aztec-nr/aztec/src/authwit/entrypoint/app.nr @@ -12,7 +12,6 @@ global ACCOUNT_MAX_CALLS: u32 = 5; // - default_entrypoint.ts // - account_entrypoint.ts (specifically `getEntrypointAbi()`) // - default_multi_call_entrypoint.ts (specifically `getEntrypointAbi()`) -// docs:start:app-payload-struct #[derive(Serialize)] pub struct AppPayload { function_calls: [FunctionCall; ACCOUNT_MAX_CALLS], diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 6026df412b1a..926c10e256d8 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -1,9 +1,5 @@ //! Standard PXE oracles. -// docs:start:oracles-module -/// Oracles module -// docs:end:oracles-module - pub mod avm; pub mod aes128_decrypt; pub mod auth_witness; diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/map.nr b/noir-projects/aztec-nr/aztec/src/state_vars/map.nr index aa4df52425eb..90d5b9164b09 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/map.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/map.nr @@ -46,7 +46,6 @@ use crate::state_vars::StateVariable; /// The storage slot derivation uses `derive_storage_slot_in_map(base_slot, key)` which computes /// `poseidon2_hash([base_slot, key.to_field()])`, ensuring cryptographically secure slot separation. /// -/// docs:start:map pub struct Map { pub context: Context, storage_slot: Field, diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr index 45edf8976f9c..cc1bbade6d6e 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr @@ -285,7 +285,6 @@ where /// The kernel will inject a unique nonce into the newly-created note, which means the new note will have a /// different nullifier, allowing it to be consumed in the future. /// - /// docs:start:get_note pub fn get_note(self) -> NoteMessage where Note: Packable, @@ -345,7 +344,6 @@ where /// /// * `Note` - The current note stored in this PrivateMutable. /// - /// docs:start:view_note pub unconstrained fn view_note(self) -> Note where Note: Packable, diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 749404d2d4cc..2745a137d094 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -161,12 +161,10 @@ impl NoteType for UintPartialNotePrivateLogContent { /// storage slot and value fields have not yet been set. A partial note can be completed in public with the `complete` /// function (revealing the storage slot and value to the public), resulting in a UintNote that can be used like any /// other one (except of course that its value is known). -// docs:start:partial_uint_note_def #[derive(Packable, Serialize, Deserialize, Eq)] pub struct PartialUintNote { commitment: Field, } -// docs:end:partial_uint_note_def impl PartialUintNote { /// Completes the partial note, creating a new note that can be used like any other UintNote. diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr index ff55d41771a5..2186e0c082ef 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr @@ -32,9 +32,7 @@ pub contract SchnorrAccount { #[storage] struct Storage { - // docs:start:public_key signing_public_key: SinglePrivateImmutable, - // docs:end:public_key } // Constructs the contract diff --git a/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr index 4f2b7dbeeb68..4f00ed62505a 100644 --- a/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr @@ -66,7 +66,6 @@ pub contract Auth { self.storage.authorized.schedule_delay_change(new_delay); } - // docs:start:get_current_value_private #[external("private")] fn do_private_authorized_thing() { // Reading a value from authorized in private automatically adds an extra validity condition: the base rollup @@ -76,7 +75,6 @@ pub contract Auth { let authorized = self.storage.authorized.get_current_value(); assert_eq(authorized, self.msg_sender(), "caller is not authorized"); } - // docs:end:get_current_value_private #[external("private")] #[view] diff --git a/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr index 816f159fa681..b5642c9776bd 100644 --- a/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr @@ -22,14 +22,12 @@ pub contract Crowdfunding { who: AztecAddress, amount: u128, } - // docs:start:storage #[storage] struct Storage { config: PublicImmutable, // Notes emitted to donors when they donate (can be used as proof to obtain rewards, eg in Claim contracts) donation_receipts: Owned, Context>, } - // docs:end:storage // TODO(#8367): Ensure deadline is quantized to improve privacy set. #[external("public")] diff --git a/noir-projects/noir-contracts/contracts/app/escrow_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/escrow_contract/src/main.nr index b3fc7d1f00d1..90df07accb10 100644 --- a/noir-projects/noir-contracts/contracts/app/escrow_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/escrow_contract/src/main.nr @@ -13,14 +13,11 @@ pub contract Escrow { use address_note::AddressNote; use token::Token; - // docs:start:single_private_immutable_storage #[storage] struct Storage { owner: SinglePrivateImmutable, } - // docs:end:single_private_immutable_storage - // docs:start:single_private_immutable_initialize // Creates a new instance #[external("private")] #[initializer] @@ -28,7 +25,6 @@ pub contract Escrow { let note = AddressNote { address: owner }; self.storage.owner.initialize(note).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); } - // docs:end:single_private_immutable_initialize // Withdraws balance. Requires that msg.sender is the owner. #[external("private")] diff --git a/noir-projects/noir-contracts/contracts/app/lending_contract/src/asset.nr b/noir-projects/noir-contracts/contracts/app/lending_contract/src/asset.nr index 9c8a96da0387..dbd2f752e62d 100644 --- a/noir-projects/noir-contracts/contracts/app/lending_contract/src/asset.nr +++ b/noir-projects/noir-contracts/contracts/app/lending_contract/src/asset.nr @@ -12,7 +12,6 @@ use std::meta::derive; /// /// Note: Right now we are wasting so many writes. If changing last_updated_ts we will end /// up rewriting all the values. -// docs:start:custom_struct_in_storage #[derive(Deserialize, Serialize)] pub struct Asset { pub interest_accumulator: u128, @@ -40,7 +39,6 @@ impl Packable for Asset { Self { interest_accumulator, last_updated_ts, loan_to_value, oracle } } } -// docs:end:custom_struct_in_storage mod test { use super::{Asset, AztecAddress, FromField, Packable}; diff --git a/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr index 242dadbbc1d3..6a6bb738c4b2 100644 --- a/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr @@ -27,7 +27,6 @@ pub contract Lending { use aztec::protocol::traits::{FromField, ToField}; - // docs:start:custom_struct_storage_map // Storage structure, containing all storage, and specifying what slots they use. #[storage] struct Storage { @@ -39,14 +38,12 @@ pub contract Lending { collateral: Map, Context>, static_debt: Map, Context>, // abusing keys very heavily } - // docs:end:custom_struct_storage_map // Constructs the contract. #[external("private")] #[initializer] fn constructor() {} - // docs:start:custom_struct_read_write #[external("public")] fn init( oracle: AztecAddress, @@ -78,7 +75,6 @@ pub contract Lending { // docs:end:public_mutable_write self.storage.stable_coin.write(stable_coin); } - // docs:end:custom_struct_read_write // Create a position. #[external("public")] diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr index b70fa9734f17..5ef30d0c3263 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr @@ -1,4 +1,3 @@ -// docs:start:imports mod types; mod test; @@ -26,8 +25,6 @@ pub contract NFT { }; use compressed_string::FieldCompressedString; - // docs:end:imports - // TODO(#8467): Rename this to Transfer - calling this NFTTransfer to avoid export conflict with the Transfer event // in the Token contract. // #[event] @@ -37,7 +34,6 @@ pub contract NFT { // token_id: Field, // } - // docs:start:storage_struct #[storage] struct Storage { // The symbol of the NFT @@ -55,9 +51,7 @@ pub contract NFT { // A map from token ID to the public owner of the NFT. public_owners: Map, Context>, } - // docs:end:storage_struct - // docs:start:constructor #[external("public")] #[initializer] fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>) { @@ -67,7 +61,6 @@ pub contract NFT { self.storage.name.initialize(FieldCompressedString::from_string(name)); self.storage.symbol.initialize(FieldCompressedString::from_string(symbol)); } - // docs:end:constructor #[external("public")] fn set_admin(new_admin: AztecAddress) { @@ -75,13 +68,11 @@ pub contract NFT { self.storage.admin.write(new_admin); } - // docs:start:set_minter #[external("public")] fn set_minter(minter: AztecAddress, approve: bool) { assert(self.storage.admin.read().eq(self.msg_sender()), "caller is not an admin"); self.storage.minters.at(minter).write(approve); } - // docs:end:set_minter #[external("public")] fn mint(to: AztecAddress, token_id: Field) { diff --git a/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr index edb2a3b1032d..1e9d2a7fb333 100644 --- a/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/private_token_contract/src/main.nr @@ -55,7 +55,6 @@ pub contract PrivateToken { // docs:start:note_delivery #[external("private")] fn mint(amount: u128, recipient: AztecAddress) { - // docs:start:owned_single_private_mutable_get_note let replacement_note_message = self.storage.admin.get_note(); let admin = replacement_note_message.get_note().address; assert(admin == self.msg_sender(), "Only admin can mint"); @@ -63,7 +62,6 @@ pub contract PrivateToken { // We deliver the new note message to the admin using unconstrained delivery, since the admin is motivated to // deliver the message to themselves (hence no need to constrain it). replacement_note_message.deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); - // docs:end:owned_single_private_mutable_get_note // We increase the total supply and once again use unconstrained delivery, since the admin is motivated to // deliver the message (he's the owner of the new note as well). self diff --git a/noir-projects/noir-contracts/contracts/app/private_voting_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/private_voting_contract/src/main.nr index 5062ff09c892..5112d2109516 100644 --- a/noir-projects/noir-contracts/contracts/app/private_voting_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/private_voting_contract/src/main.nr @@ -3,7 +3,6 @@ use aztec::macros::aztec; #[aztec] pub contract PrivateVoting { - // docs:start:imports use aztec::macros::{functions::{external, initializer, only_self, view}, storage::storage}; use aztec::protocol::{address::AztecAddress, traits::{Deserialize, Serialize, ToField}}; use aztec::state_vars::{Map, Owned, PublicImmutable, PublicMutable, SingleUseClaim}; @@ -25,8 +24,6 @@ pub contract PrivateVoting { } } - // docs:end:imports - // docs:start:storage_struct #[storage] struct Storage { // admin can start and end elections @@ -40,15 +37,12 @@ pub contract PrivateVoting { // election => voter => single use claim that ensures voter can at most vote once per election vote_claims: Map, Context>, Context>, } - // docs:end:storage_struct - // docs:start:constructor #[external("public")] #[initializer] fn constructor(admin: AztecAddress) { self.storage.admin.write(admin); } - // docs:end:constructor #[external("private")] fn cast_vote(election_id: ElectionId, candidate: Field) { @@ -56,7 +50,6 @@ pub contract PrivateVoting { self.enqueue_self.add_to_tally_public(election_id, candidate); } - // docs:start:nested_map_access #[external("public")] #[only_self] fn add_to_tally_public(election_id: ElectionId, candidate: Field) { @@ -65,7 +58,6 @@ pub contract PrivateVoting { let new_tally = self.storage.tally.at(election_id).at(candidate).read() + 1; self.storage.tally.at(election_id).at(candidate).write(new_tally); } - // docs:end:nested_map_access #[external("public")] fn start_vote(election_id: ElectionId) { diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index 08aca0350095..a9bee6b0b4a6 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -61,7 +61,6 @@ pub contract TokenBlacklist { roles: Map, Context>, } - // docs:start:constructor #[external("public")] #[initializer] fn constructor(admin: AztecAddress) { @@ -221,7 +220,6 @@ pub contract TokenBlacklist { self.enqueue_self._increase_public_balance(to, amount); } - // docs:start:transfer_private #[authorize_once("from", "authwit_nonce")] #[external("private")] fn transfer(from: AztecAddress, to: AztecAddress, amount: u128, authwit_nonce: Field) { diff --git a/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr index f3eb8b9f10f7..eb63074d7848 100644 --- a/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr @@ -114,7 +114,6 @@ pub contract TokenBridge { self.call(Token::at(config.token).mint_to_private(recipient, amount)); } - // docs:start:exit_to_l1_private // Burns the appropriate amount of tokens and creates a L2 to L1 withdraw message privately // Requires `msg.sender` (caller of the method) to give approval to the bridge to burn tokens on their behalf using witness signatures #[external("private")] @@ -137,5 +136,4 @@ pub contract TokenBridge { // Burn tokens self.call(Token::at(token).burn_private(self.msg_sender(), amount, authwit_nonce)); } - // docs:end:exit_to_l1_private } diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr index 7f77a2c3101c..4c17ecf9a6f7 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr @@ -1,4 +1,3 @@ -// docs:start:imports mod test; use aztec::macros::aztec; @@ -33,8 +32,6 @@ pub contract Token { use balance_set::BalanceSet; - // docs:end::imports - // In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving // keys, etc.), so the gate count is already relatively high. We therefore only read a few notes to keep the happy // case with few constraints. @@ -51,7 +48,6 @@ pub contract Token { amount: u128, } - // docs:start:storage_struct #[storage] struct Storage { admin: PublicMutable, @@ -63,9 +59,7 @@ pub contract Token { name: PublicImmutable, decimals: PublicImmutable, } - // docs:end:storage_struct - // docs:start:constructor #[external("public")] #[initializer] fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { @@ -76,7 +70,6 @@ pub contract Token { self.storage.symbol.initialize(FieldCompressedString::from_string(symbol)); self.storage.decimals.initialize(decimals); } - // docs:end:constructor #[external("public")] fn set_admin(new_admin: AztecAddress) { @@ -84,7 +77,6 @@ pub contract Token { self.storage.admin.write(new_admin); } - // docs:start:public_immutable_read #[external("public")] #[view] fn public_get_name() -> FieldCompressedString { @@ -96,7 +88,6 @@ pub contract Token { fn private_get_name() -> FieldCompressedString { self.storage.name.read() } - // docs:end:public_immutable_read #[external("public")] #[view] @@ -312,9 +303,7 @@ pub contract Token { amount: u128, authwit_nonce: Field, ) { - // docs:start:increase_private_balance self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); - // docs:end:increase_private_balance self.storage.balances.at(to).add(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); } // docs:end:transfer_in_private @@ -352,14 +341,12 @@ pub contract Token { /// us to have it inlined in the `transfer_to_private` function which results in one fewer kernel iteration. Note /// that in this case we don't pass `completer` as an argument to this function because in all the callsites we /// want to use the message sender as the completer anyway. - // docs:start:prepare_private_balance_increase #[internal("private")] fn _prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { let partial_note = UintNote::partial(to, self.context, to, self.msg_sender()); partial_note } - // docs:end:prepare_private_balance_increase /// Finalizes a transfer of token `amount` from public balance of `msg_sender` to a private balance of `to`. /// The transfer must be prepared by calling `prepare_private_balance_increase` from `msg_sender` account and @@ -420,7 +407,6 @@ pub contract Token { // In all the flows in this contract, `from` (the account from which we're subtracting the `amount`) and // `completer` (the entity that can complete the partial note) are the same so we represent them with a single // argument. - // docs:start:finalize_transfer_to_private #[internal("public")] fn _finalize_transfer_to_private( from_and_completer: AztecAddress, @@ -441,7 +427,6 @@ pub contract Token { amount, ); } - // docs:end:finalize_transfer_to_private /// Mints token `amount` to a private balance of `to`. Message sender has to have minter permissions (checked /// in the enqueued call). diff --git a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr index d4b690e1f9c4..dd0fe64eea4a 100644 --- a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr @@ -1,4 +1,3 @@ -// docs:start:uniswap_setup mod util; // Demonstrates how to use portal contracts to swap on L1 Uniswap with funds on L2 @@ -36,9 +35,7 @@ pub contract Uniswap { fn constructor(portal_address: EthAddress) { self.storage.portal_address.initialize(portal_address); } - // docs:end:uniswap_setup - // docs:start:swap_public #[external("public")] fn swap_public( sender: AztecAddress, @@ -107,9 +104,7 @@ pub contract Uniswap { ); self.context.message_portal(self.storage.portal_address.read(), content_hash); } - // docs:end:swap_public - // docs:start:swap_private #[external("private")] fn swap_private( input_asset: AztecAddress, // since private, we pass here and later assert that this is as expected by input_bridge @@ -175,7 +170,6 @@ pub contract Uniswap { ); self.context.message_portal(self.storage.portal_address.read(), content_hash); } - // docs:end:swap_private // docs:start:authwit_uniswap_set // This helper method approves the bridge to burn this contract's funds and exits the input asset to L1 diff --git a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/util.nr b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/util.nr index df44cfc9d50c..0398530870d4 100644 --- a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/util.nr +++ b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/util.nr @@ -1,4 +1,3 @@ -// docs:start:uniswap_public_content_hash use aztec::protocol::{address::{AztecAddress, EthAddress}, hash::sha256_to_field, traits::ToField}; // This method computes the L2 to L1 message content hash for the public @@ -54,9 +53,7 @@ pub fn compute_swap_public_content_hash( let content_hash = sha256_to_field(hash_bytes); content_hash } -// docs:end:uniswap_public_content_hash -// docs:start:compute_swap_private_content_hash // This method computes the L2 to L1 message content hash for the private // refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected pub fn compute_swap_private_content_hash( @@ -106,4 +103,3 @@ pub fn compute_swap_private_content_hash( let content_hash = sha256_to_field(hash_bytes); content_hash } -// docs:end:compute_swap_private_content_hash diff --git a/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr index c0b209eb6451..a5e4647a0f57 100644 --- a/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/fees/fpc_contract/src/main.nr @@ -82,7 +82,6 @@ pub contract FPC { /// - the `max_fee`, /// - which FPC has been used to make the payment, /// - the asset which was used to make the payment. - // docs:start:fee_entrypoint_private #[external("private")] #[allow_phase_change] fn fee_entrypoint_private(max_fee: u128, authwit_nonce: Field) { @@ -112,11 +111,9 @@ pub contract FPC { // End the setup phase, from now on all side effects are revertible self.context.end_setup(); } - // docs:end:fee_entrypoint_private /// Executed as a public teardown function and is responsible for completing the refund in the private fee payment /// flow. - // docs:start:complete_refund #[external("public")] #[only_self] fn _complete_refund( @@ -138,7 +135,6 @@ pub contract FPC { self.context, ); } - // docs:end:complete_refund /// Pays for the tx fee with msg_sender's public balance of accepted asset (AA). The maximum fee a user is willing /// to pay is defined by `max_fee` and is denominated in AA. diff --git a/noir-projects/noir-contracts/contracts/test/counter/counter_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/counter/counter_contract/src/main.nr index 87260f157ad1..623b46b8c919 100644 --- a/noir-projects/noir-contracts/contracts/test/counter/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/counter/counter_contract/src/main.nr @@ -1,10 +1,7 @@ -// docs:start:setup use aztec::macros::aztec; #[aztec] pub contract Counter { - // docs:end:setup - // docs:start:imports use aztec::{ macros::{functions::{external, initializer}, storage::storage}, messages::message_delivery::MessageDelivery, @@ -13,16 +10,12 @@ pub contract Counter { state_vars::Owned, }; use balance_set::BalanceSet; - // docs:end:imports - // docs:start:storage_struct #[storage] struct Storage { counters: Owned, Context>, } - // docs:end:storage_struct - // docs:start:constructor #[initializer] #[external("private")] // We can name our initializer anything we want as long as it's marked as aztec(initializer) @@ -31,15 +24,12 @@ pub contract Counter { MessageDelivery.ONCHAIN_CONSTRAINED, ); } - // docs:end:constructor - // docs:start:increment #[external("private")] fn increment(owner: AztecAddress) { debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]); self.storage.counters.at(owner).add(1).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); } - // docs:end:increment #[external("private")] fn increment_twice(owner: AztecAddress) { @@ -67,12 +57,10 @@ pub contract Counter { self.storage.counters.at(owner).sub(1).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); } - // docs:start:get_counter #[external("utility")] unconstrained fn get_counter(owner: AztecAddress) -> u128 { self.storage.counters.at(owner).balance_of() } - // docs:end:get_counter #[external("private")] fn increment_self_and_other(other_counter: AztecAddress, owner: AztecAddress) { diff --git a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr index 2d7a956cfd0d..05ac54c3c826 100644 --- a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr +++ b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr @@ -1,6 +1,4 @@ -// docs:start:custom_filter_imports use aztec::{note::HintedNote, protocol::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL}; -// docs:end:custom_filter_imports use field_note::FieldNote; // docs:start:custom_filter diff --git a/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr b/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr index 082b8af6630f..96ee60690b9d 100644 --- a/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr +++ b/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr @@ -1,6 +1,5 @@ use crate::{reader::Reader, writer::Writer}; -// docs:start:serialize /// Trait for serializing Noir types into arrays of Fields. /// /// An implementation of the Serialize trait has to follow Noir's intrinsic serialization (each member of a struct @@ -125,7 +124,6 @@ pub comptime fn derive_serialize(s: TypeDefinition) -> Quoted { } } -// docs:start:deserialize /// Trait for deserializing Noir types from arrays of Fields. /// /// An implementation of the Deserialize trait has to follow Noir's intrinsic serialization (each member of a struct diff --git a/yarn-project/cli-wallet/test/flows/basic.sh b/yarn-project/cli-wallet/test/flows/basic.sh index 08b9573c4eb0..600ca9f76078 100755 --- a/yarn-project/cli-wallet/test/flows/basic.sh +++ b/yarn-project/cli-wallet/test/flows/basic.sh @@ -6,9 +6,7 @@ test_title "Basic flow" AMOUNT=42 -# docs:start:import-test-accounts aztec-wallet import-test-accounts -# docs:end:import-test-accounts aztec-wallet deploy token_contract@Token --args accounts:test0 Test TST 18 -f test0 aztec-wallet send mint_to_public -ca last --args accounts:test0 $AMOUNT -f test0 RESULT=$(aztec-wallet simulate balance_of_public -ca last --args accounts:test0 -f test0 | grep "Simulation result:" | awk '{print $3}') diff --git a/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh b/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh index 142e8281340f..fde435201086 100755 --- a/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh +++ b/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh @@ -5,10 +5,8 @@ source shared/setup.sh test_title "Create an account and deploy using native fee payment with bridging" -# docs:start:bridge-fee-juice aztec-wallet create-account -a main --register-only aztec-wallet bridge-fee-juice 1000000000000000000000 main --mint --no-wait -# docs:end:bridge-fee-juice section "Use a pre-funded test account to send dummy txs to force block creations" @@ -19,9 +17,7 @@ aztec-wallet send increment -ca counter --args accounts:test0 -f test0 section "Deploy main account claiming the fee juice, use it later" -# docs:start:claim-deploy-account aztec-wallet deploy-account main --payment method=fee_juice,claim -# docs:end:claim-deploy-account aztec-wallet send increment -ca counter --args accounts:main -f main aztec-wallet send increment -ca counter --args accounts:main -f main diff --git a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts index 966c73aafe4b..f6e8824ecc3c 100644 --- a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts @@ -34,7 +34,6 @@ const { AZTEC_NODE_URL = 'http://localhost:8080' } = process.env; describe('e2e_local_network_example', () => { it('local network example works', async () => { ////////////// CREATE THE CLIENT INTERFACE AND CONTACT THE LOCAL NETWORK ////////////// - // docs:start:setup const logger = createLogger('e2e:token'); // We create PXE client connected to the local network URL @@ -47,8 +46,6 @@ describe('e2e_local_network_example', () => { logger.info(format('Aztec Local Network Info ', nodeInfo)); - // docs:end:setup - expect(typeof nodeInfo.rollupVersion).toBe('number'); expect(typeof nodeInfo.l1ChainId).toBe('number'); expect(typeof nodeInfo.l1ContractAddresses.rollupAddress).toBe('object'); @@ -182,7 +179,6 @@ describe('e2e_local_network_example', () => { ////////////// USE A NEW ACCOUNT TO SEND A TX AND PAY WITH BANANA COIN ////////////// const amountTransferToBob = 100n; const bananaFPCAddress = await registerDeployedBananaFPCInWalletAndGetAddress(wallet); - // docs:start:private_fpc_payment // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay const maxFeesPerGas = (await node.getCurrentMinFees()).mul(1.5); @@ -191,7 +187,6 @@ describe('e2e_local_network_example', () => { const { receipt: receiptForAlice } = await bananaCoin.methods .transfer(bob, amountTransferToBob) .send({ from: alice, fee: { paymentMethod } }); - // docs:end:private_fpc_payment const transactionFee = receiptForAlice.transactionFee!; logger.info(`Transaction fee: ${transactionFee}`); @@ -208,7 +203,6 @@ describe('e2e_local_network_example', () => { const amountTransferToAlice = 48n; const sponsoredFPC = await registerDeployedSponsoredFPCInWalletAndGetAddress(wallet); - // docs:start:sponsored_fpc_payment const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC); // The payment method can also be initialized as follows: // const sponsoredPaymentMethod = await SponsoredFeePaymentMethod.new(pxe); @@ -217,7 +211,6 @@ describe('e2e_local_network_example', () => { const { receipt: receiptForBob } = await bananaCoin.methods .transfer(alice, amountTransferToAlice) .send({ from: bob, fee: { paymentMethod: sponsoredPaymentMethod } }); - // docs:end:sponsored_fpc_payment // Check the balances const { result: aliceNewBalance } = await bananaCoin.methods.balance_of_private(alice).simulate({ from: alice }); logger.info(`Alice's new balance: ${aliceNewBalance}`); diff --git a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts index 5c5243df8c66..532d25ee5832 100644 --- a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts @@ -1,5 +1,4 @@ // This test should only use packages that are published to npm -// docs:start:imports import { EthAddress } from '@aztec/aztec.js/addresses'; import { waitForProven } from '@aztec/aztec.js/contracts'; import { L1TokenManager, L1TokenPortalManager } from '@aztec/aztec.js/ethereum'; @@ -25,8 +24,6 @@ import { getContract } from 'viem'; import { TestWallet } from '../test-wallet/test_wallet.js'; -// docs:end:imports -// docs:start:utils const MNEMONIC = 'test test test test test test test test test test test junk'; const { ETHEREUM_HOSTS = 'http://localhost:8545' } = process.env; @@ -71,7 +68,6 @@ async function addMinter(l1TokenContract: EthAddress, l1TokenHandler: EthAddress }); await contract.write.addMinter([l1TokenHandler.toString()]); } -// docs:end:utils // To run these tests against a local network: // 1. Start a local Ethereum node (Anvil): @@ -85,7 +81,6 @@ async function addMinter(l1TokenContract: EthAddress, l1TokenHandler: EthAddress // yarn test:e2e e2e_token_bridge_tutorial_test.test.ts describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { it('Deploys tokens & bridges to L1 & L2, mints & publicly bridges tokens', async () => { - // docs:start:setup const logger = createLogger('aztec:token-bridge-tutorial'); const { wallet, node } = await setupLocalNetwork(); const [ownerAztecAddress] = await registerInitialLocalNetworkAccountsInWallet(wallet); @@ -95,10 +90,8 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { logger.info(`Inbox Address: ${l1ContractAddresses.inboxAddress}`); logger.info(`Outbox Address: ${l1ContractAddresses.outboxAddress}`); logger.info(`Rollup Address: ${l1ContractAddresses.rollupAddress}`); - // docs:end:setup // Deploy L2 token contract - // docs:start:deploy-l2-token const { contract: l2TokenContract } = await TokenContract.deploy( wallet, ownerAztecAddress, @@ -109,10 +102,8 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { from: ownerAztecAddress, }); logger.info(`L2 token contract deployed at ${l2TokenContract.address}`); - // docs:end:deploy-l2-token // Deploy L1 token contract & mint tokens - // docs:start:deploy-l1-token const l1TokenContract = await deployTestERC20(); logger.info('erc20 contract deployed'); @@ -120,10 +111,8 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { await addMinter(l1TokenContract, feeAssetHandler); const l1TokenManager = new L1TokenManager(l1TokenContract, feeAssetHandler, l1Client, logger); - // docs:end:deploy-l1-token // Deploy L1 portal contract - // docs:start:deploy-portal const l1PortalContractAddress = await deployTokenPortal(); logger.info('L1 portal contract deployed'); @@ -132,24 +121,18 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { abi: TokenPortalAbi, client: l1Client, }); - // docs:end:deploy-portal // Deploy L2 bridge contract - // docs:start:deploy-l2-bridge const { contract: l2BridgeContract } = await TokenBridgeContract.deploy( wallet, l2TokenContract.address, l1PortalContractAddress, ).send({ from: ownerAztecAddress }); logger.info(`L2 token bridge contract deployed at ${l2BridgeContract.address}`); - // docs:end:deploy-l2-bridge // Set Bridge as a minter - // docs:start:authorize-l2-bridge await l2TokenContract.methods.set_minter(l2BridgeContract.address, true).send({ from: ownerAztecAddress }); - // docs:end:authorize-l2-bridge // Initialize L1 portal contract - // docs:start:setup-portal await l1Portal.write.initialize( [l1ContractAddresses.registryAddress.toString(), l1TokenContract.toString(), l2BridgeContract.address.toString()], {}, @@ -164,19 +147,15 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { l1Client, logger, ); - // docs:end:setup-portal - // docs:start:l1-bridge-public const claim = await l1PortalManager.bridgeTokensPublic(ownerAztecAddress, MINT_AMOUNT, true); // Do 2 unrelated actions because // https://github.com/AztecProtocol/aztec-packages/blob/7e9e2681e314145237f95f79ffdc95ad25a0e319/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts#L354-L355 await l2TokenContract.methods.mint_to_public(ownerAztecAddress, 0n).send({ from: ownerAztecAddress }); await l2TokenContract.methods.mint_to_public(ownerAztecAddress, 0n).send({ from: ownerAztecAddress }); - // docs:end:l1-bridge-public // Claim tokens publicly on L2 - // docs:start:claim await l2BridgeContract.methods .claim_public(ownerAztecAddress, MINT_AMOUNT, claim.claimSecret, claim.messageLeafIndex) .send({ from: ownerAztecAddress }); @@ -184,11 +163,9 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { .balance_of_public(ownerAztecAddress) .simulate({ from: ownerAztecAddress }); logger.info(`Public L2 balance of ${ownerAztecAddress} is ${balance}`); - // docs:end:claim logger.info('Withdrawing funds from L2'); - // docs:start:setup-withdrawal const withdrawAmount = 9n; const authwitNonce = Fr.random(); @@ -202,9 +179,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { true, ); await authwit.send(); - // docs:end:setup-withdrawal - // docs:start:l2-withdraw const l2ToL1Message = await l1PortalManager.getL2ToL1MessageLeaf( withdrawAmount, EthAddress.fromString(ownerEthAddress), @@ -220,9 +195,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { .balance_of_public(ownerAztecAddress) .simulate({ from: ownerAztecAddress }); logger.info(`New L2 balance of ${ownerAztecAddress} is ${newL2Balance}`); - // docs:end:l2-withdraw - // docs:start:l1-withdraw const result = await computeL2ToL1MembershipWitness(node, l2ToL1Message, l2TxReceipt.txHash); if (!result) { throw new Error('L2 to L1 message not found'); @@ -237,7 +210,6 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { ); const newL1Balance = await l1TokenManager.getL1TokenBalance(ownerEthAddress); logger.info(`New L1 balance of ${ownerEthAddress} is ${newL1Balance}`); - // docs:end:l1-withdraw expect(newL1Balance).toBe(withdrawAmount); }, 300_000); }); diff --git a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts index 3845a3e3d916..a614516846dd 100644 --- a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts @@ -9,7 +9,6 @@ const EXPECTED_FORKED_BLOCK = 0; //17514288; let teardown: () => Promise; -// docs:start:uniswap_setup const testSetup = async () => { const context = await e2eSetup(2, { stateLoad: dumpedState, startProverNode: true }); @@ -17,7 +16,6 @@ const testSetup = async () => { return context; }; -// docs:end:uniswap_setup const testCleanup = async () => { await teardown(); diff --git a/yarn-project/end-to-end/src/guides/up_quick_start.sh b/yarn-project/end-to-end/src/guides/up_quick_start.sh index d266aed3ac84..69dc87799fed 100755 --- a/yarn-project/end-to-end/src/guides/up_quick_start.sh +++ b/yarn-project/end-to-end/src/guides/up_quick_start.sh @@ -18,10 +18,8 @@ aztec-wallet() { aztec-wallet import-test-accounts -# docs:start:declare-accounts aztec-wallet create-account -a alice -f test0 aztec-wallet create-account -a bob -f test0 -# docs:end:declare-accounts aztec-wallet bridge-fee-juice 1000000000000000000000 accounts:alice --mint --no-wait diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index 2e8426fa1351..4ba87e8bc1d0 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -178,4 +178,3 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { } } } -// docs:end:cross_chain_test_harness diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index b75f1e8b76d3..1950d9925e4a 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -154,7 +154,6 @@ export const uniswapL1L2TestSuite = ( await cleanup(); }); - // docs:start:uniswap_private it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); @@ -345,10 +344,8 @@ export const uniswapL1L2TestSuite = ( logger.info('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); logger.info('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); }); - // docs:end:uniswap_private // TODO(#7463): reenable look into this failure https://github.com/AztecProtocol/aztec-packages/actions/runs/9912612912/job/27388320150?pr=7462 - // // docs:start:uniswap_public // it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => { // const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); @@ -581,7 +578,6 @@ export const uniswapL1L2TestSuite = ( // logger.info('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); // logger.info('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); // }); - // // docs:end:uniswap_public // Edge cases for the private flow: // note - tests for uniswapPortal.sol and minting asset on L2 are covered in other tests. diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index 2db7b6038255..57563145b9ed 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -309,7 +309,6 @@ export class ContractFunctionSimulator { } } - // docs:start:execute_utility_function /** * Runs a utility function. * @param call - The function call to execute. @@ -384,7 +383,6 @@ export class ContractFunctionSimulator { throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution')); } } - // docs:end:execute_utility_function /** * Returns the execution statistics collected during the simulator run. From 4f1fc521d9b7072dd897ea59d62614e833ff48d2 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 21 Apr 2026 19:16:45 +0000 Subject: [PATCH 12/14] fix: resolve cherry-pick conflicts - Accept upstream apiref-link content in the two versioned docs files, keeping them at the v4.1.0-rc.2 path (v4-next has no v4.2.0 directory). - Drop stale `// docs:end:imports` marker in recursive_verification index.ts, keeping the `assert` import which is still used. - Delete the webapp-tutorial and solidity/example_swap example trees that were already removed on v4-next (they were re-added by the modify/delete side of the cherry-pick). --- .../advanced/protocol_oracles.md | 17 - .../docs/aztec-nr/testing_contracts.md | 4 - .../solidity/example_swap/ExampleERC20.sol | 15 - .../example_swap/ExampleTokenPortal.sol | 108 -- .../ts/recursive_verification/index.ts | 4 - .../webapp-tutorial/contracts/src/main.nr | 182 --- docs/examples/webapp-tutorial/src/App.tsx | 131 -- .../src/components/ErrorBoundary.tsx | 47 - .../src/components/GameBoard.tsx | 151 -- .../src/components/GameLobby.tsx | 154 -- .../src/components/GameStatus.tsx | 36 - .../src/components/TxStatus.tsx | 34 - .../src/components/WalletConnect.tsx | 198 --- docs/examples/webapp-tutorial/src/config.ts | 33 - docs/examples/webapp-tutorial/src/contract.ts | 120 -- .../webapp-tutorial/src/embedded-wallet.ts | 144 -- docs/examples/webapp-tutorial/src/fees.ts | 47 - .../webapp-tutorial/src/game-constants.ts | 5 - .../webapp-tutorial/src/wallet-connection.ts | 112 -- .../test-extension/src/account-utils.ts | 60 - .../test-extension/src/aztec-imports.ts | 125 -- .../test-extension/src/background.ts | 1236 ----------------- .../test-extension/src/config.ts | 96 -- .../test-extension/src/offscreen/offscreen.ts | 771 ---------- .../src/popup/AccountSwitcher.tsx | 51 - .../test-extension/src/popup/ApprovalView.tsx | 509 ------- .../src/popup/CreateAccountView.tsx | 49 - .../test-extension/src/popup/Header.tsx | 104 -- .../test-extension/src/popup/LockScreen.tsx | 60 - .../test-extension/src/popup/SettingsPage.tsx | 154 -- .../test-extension/src/popup/SetupScreen.tsx | 130 -- .../test-extension/src/popup/helpers.ts | 100 -- .../test-extension/src/popup/popup.tsx | 467 ------- .../test-extension/src/popup/types.ts | 18 - .../test-extension/src/shared-types.ts | 97 -- .../test-extension/src/utils.ts | 44 - .../test-extension/src/wallet/storage.ts | 333 ----- .../test-extension/src/wallet/wallet-impl.ts | 153 -- 38 files changed, 6099 deletions(-) delete mode 100644 docs/examples/solidity/example_swap/ExampleERC20.sol delete mode 100644 docs/examples/solidity/example_swap/ExampleTokenPortal.sol delete mode 100644 docs/examples/webapp-tutorial/contracts/src/main.nr delete mode 100644 docs/examples/webapp-tutorial/src/App.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/GameBoard.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/GameLobby.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/GameStatus.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/TxStatus.tsx delete mode 100644 docs/examples/webapp-tutorial/src/components/WalletConnect.tsx delete mode 100644 docs/examples/webapp-tutorial/src/config.ts delete mode 100644 docs/examples/webapp-tutorial/src/contract.ts delete mode 100644 docs/examples/webapp-tutorial/src/embedded-wallet.ts delete mode 100644 docs/examples/webapp-tutorial/src/fees.ts delete mode 100644 docs/examples/webapp-tutorial/src/game-constants.ts delete mode 100644 docs/examples/webapp-tutorial/src/wallet-connection.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/account-utils.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/background.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/config.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/popup/types.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/shared-types.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/utils.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts delete mode 100644 docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md index 9a33852f51f2..f45015b8923c 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md @@ -21,22 +21,6 @@ If we fetch the notes using an oracle call, we can keep the function signature i Oracles introduce **non-determinism** into a circuit, and thus are `unconstrained`. It is important that any information that is injected into a circuit through an oracle is later constrained for correctness. Otherwise, the circuit will be **under-constrained** and potentially insecure! -<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/framework-description/advanced/protocol_oracles.md -`Aztec.nr` has a module dedicated to its oracles. If you are interested, you can view them by following the link below: -```rust title="oracles-module" showLineNumbers -/// Oracles module -``` -> Source code: noir-projects/aztec-nr/aztec/src/oracle/mod.nr#L3-L5 - - -## Inbuilt oracles - -- [`debug_log`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/noir-protocol-circuits/crates/types/src/debug_log.nr) - Provides debug functions that can be used to log information to the console. Read more about debugging [here](../../debugging.md). -- [`auth_witness`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. -- [`get_l1_to_l2_membership_witness`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_membership_witness.nr) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. -- [`notes`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/notes.nr) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. -- [`logs`](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle/logs.nr) - Provides functions to log encrypted and unencrypted data. -======= `Aztec.nr` has a [module dedicated to its oracles](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/index.html) where you can browse the full list. ## Inbuilt oracles @@ -46,7 +30,6 @@ Oracles introduce **non-determinism** into a circuit, and thus are `unconstraine - [`get_l1_to_l2_membership_witness`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/get_l1_to_l2_membership_witness/index.html) - Returns the leaf index and sibling path for an L1 to L2 message, used to prove message existence in cross-chain applications like token bridges. - [`notes`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/notes/index.html) - Provides functions related to notes, such as fetching notes from storage, used behind the scenes for value notes and other pre-built note implementations. - [`logs`](pathname:///aztec-nr-api/mainnet/noir_aztec/oracle/logs/index.html) - Provides functions to log encrypted and unencrypted data. ->>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-nr/framework-description/advanced/protocol_oracles.md Find a full list [on GitHub](https://github.com/AztecProtocol/aztec-packages/tree/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/oracle). diff --git a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md index 1e8bd3f4e74c..5401e2f6a6d4 100644 --- a/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md +++ b/docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md @@ -67,11 +67,7 @@ unconstrained fn test_basic_flow() { - Tests run in parallel by default - Use `unconstrained` functions for faster execution -<<<<<<< HEAD:docs/developer_versioned_docs/version-v4.1.0-rc.2/docs/aztec-nr/testing_contracts.md -- See all `TestEnvironment` methods [here](https://github.com/AztecProtocol/aztec-packages/blob/v4.1.0-rc.2/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr) -======= - See all `TestEnvironment` methods [here](pathname:///aztec-nr-api/mainnet/noir_aztec/test/helpers/test_environment/struct.TestEnvironment) ->>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)):docs/developer_versioned_docs/version-v4.2.0/docs/aztec-nr/testing_contracts.md ::: diff --git a/docs/examples/solidity/example_swap/ExampleERC20.sol b/docs/examples/solidity/example_swap/ExampleERC20.sol deleted file mode 100644 index 6cbaa356f515..000000000000 --- a/docs/examples/solidity/example_swap/ExampleERC20.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.27; - -import {ERC20} from "@oz/token/ERC20/ERC20.sol"; - -/// @title ExampleERC20 -/// @notice Minimal ERC20 with public mint for testing L1<>L2 swap flows. -contract ExampleERC20 is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} - - /// @notice Anyone can mint tokens (test only!) - function mint(address to, uint256 amount) external { - _mint(to, amount); - } -} diff --git a/docs/examples/solidity/example_swap/ExampleTokenPortal.sol b/docs/examples/solidity/example_swap/ExampleTokenPortal.sol deleted file mode 100644 index 5e728b795604..000000000000 --- a/docs/examples/solidity/example_swap/ExampleTokenPortal.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.27; - -// docs:start:example_token_portal -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; -import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; -import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; -import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; -import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; -import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; -import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; -import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; - -/// @title ExampleTokenPortal -/// @notice Example token portal for tutorial. -contract ExampleTokenPortal { - using SafeERC20 for IERC20; - - IRegistry public registry; - IERC20 public underlying; - bytes32 public l2Bridge; - - IRollup public rollup; - IOutbox public outbox; - IInbox public inbox; - uint256 public rollupVersion; - - /// @dev No access control for simplicity. A production contract should restrict this to the deployer/owner. - function initialize( - address _registry, - address _underlying, - bytes32 _l2Bridge - ) external { - registry = IRegistry(_registry); - underlying = IERC20(_underlying); - l2Bridge = _l2Bridge; - - rollup = IRollup(address(registry.getCanonicalRollup())); - outbox = rollup.getOutbox(); - inbox = rollup.getInbox(); - rollupVersion = rollup.getVersion(); - } - // docs:end:example_token_portal - - // docs:start:deposit_to_aztec_public - /// @notice Deposit tokens and send L1->L2 message for public minting on Aztec - function depositToAztecPublic( - bytes32 _to, - uint256 _amount, - bytes32 _secretHash - ) external returns (bytes32, uint256) { - DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); - - bytes32 contentHash = Hash.sha256ToField( - abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, _amount) - ); - - underlying.safeTransferFrom(msg.sender, address(this), _amount); - - return inbox.sendL2Message(actor, contentHash, _secretHash); - } - // docs:end:deposit_to_aztec_public - - /// @notice Deposit tokens and send L1->L2 message for private minting on Aztec - function depositToAztecPrivate( - uint256 _amount, - bytes32 _secretHash - ) external returns (bytes32, uint256) { - DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion); - - bytes32 contentHash = Hash.sha256ToField( - abi.encodeWithSignature("mint_to_private(uint256)", _amount) - ); - - underlying.safeTransferFrom(msg.sender, address(this), _amount); - - return inbox.sendL2Message(actor, contentHash, _secretHash); - } - - // docs:start:withdraw - /// @notice Withdraw tokens after consuming an L2->L1 message. - function withdraw( - address _recipient, - uint256 _amount, - Epoch _epoch, - uint256 _leafIndex, - bytes32[] calldata _path - ) external { - DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ - sender: DataStructures.L2Actor(l2Bridge, rollupVersion), - recipient: DataStructures.L1Actor(address(this), block.chainid), - content: Hash.sha256ToField( - abi.encodeWithSignature( - "withdraw(address,uint256,address)", - _recipient, - _amount, - msg.sender - ) - ) - }); - - outbox.consume(message, _epoch, _leafIndex, _path); - - underlying.safeTransfer(_recipient, _amount); - } - // docs:end:withdraw -} diff --git a/docs/examples/ts/recursive_verification/index.ts b/docs/examples/ts/recursive_verification/index.ts index 15c19c66675c..cf6c1429f532 100644 --- a/docs/examples/ts/recursive_verification/index.ts +++ b/docs/examples/ts/recursive_verification/index.ts @@ -8,11 +8,7 @@ import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { NO_FROM } from "@aztec/aztec.js/account"; import { Fr } from "@aztec/aztec.js/fields"; import fs from "node:fs"; -<<<<<<< HEAD import assert from "node:assert"; -// docs:end:imports -======= ->>>>>>> d84d562134 (docs: link to apiref, not gh, remove stale include code markers (#22649)) if (!fs.existsSync("data.json")) { console.error( diff --git a/docs/examples/webapp-tutorial/contracts/src/main.nr b/docs/examples/webapp-tutorial/contracts/src/main.nr deleted file mode 100644 index 872daafd4370..000000000000 --- a/docs/examples/webapp-tutorial/contracts/src/main.nr +++ /dev/null @@ -1,182 +0,0 @@ -// Pod Racing Game Contract -// -// A two-player competitive racing game where players allocate points across 5 tracks -// over multiple rounds. The game flow: -// 1. Player 1 creates a game with a time limit -// 2. Player 2 joins the game -// 3. Both players play rounds privately (allocating points across tracks) -// 4. After all rounds, players reveal their total scores per track -// 5. Winner is determined by who won more tracks (best of 5) -// -// Key mechanics: -// - Each round, players distribute up to 9 points across 5 tracks -// - Round choices are private until the finish phase -// - The player with higher total points on a track wins that track -// - The player who wins 3+ tracks wins the game - -mod game_round_note; -mod race; - -use aztec::macros::aztec; - -#[aztec] -pub contract PodRacing { - use aztec::{ - macros::{functions::{external, initializer, only_self}, storage::storage}, - messages::message_delivery::MessageDelivery, - note::note_getter_options::NoteGetterOptions, - }; - use aztec::protocol::address::AztecAddress; - use aztec::state_vars::{Map, Owned, PrivateSet, PublicMutable}; - - use crate::{game_round_note::GameRoundNote, race::Race}; - - global TOTAL_ROUNDS: u8 = 3; - global GAME_LENGTH: u32 = 2; // Set to 2 for demo purposes - - // docs:start:storage - #[storage] - struct Storage { - admin: PublicMutable, - races: Map, Context>, - progress: Map, Context>, Context>, - win_history: Map, Context>, - } - // docs:end:storage - - #[external("public")] - #[initializer] - fn constructor(admin: AztecAddress) { - self.storage.admin.write(admin); - } - - // docs:start:create-game - #[external("public")] - fn create_game(game_id: Field) { - assert(self.storage.races.at(game_id).read().player1.eq(AztecAddress::zero())); - let game = Race::new( - self.msg_sender(), - TOTAL_ROUNDS, - self.context.block_number() + GAME_LENGTH, - ); - self.storage.races.at(game_id).write(game); - } - // docs:end:create-game - - // docs:start:join-game - #[external("public")] - fn join_game(game_id: Field) { - let maybe_existing_game = self.storage.races.at(game_id).read(); - let joined_game = maybe_existing_game.join(self.msg_sender()); - self.storage.races.at(game_id).write(joined_game); - } - // docs:end:join-game - - // docs:start:play-round - /// Allocates points across 5 tracks for a round. - /// This is a PRIVATE function - the allocation remains hidden from the opponent. - #[external("private")] - fn play_round( - game_id: Field, - round: u8, - track1: u8, - track2: u8, - track3: u8, - track4: u8, - track5: u8, - ) { - assert(track1 + track2 + track3 + track4 + track5 < 10); - - let player = self.msg_sender(); - - self - .storage - .progress - .at(game_id) - .at(player) - .insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player)) - .deliver(MessageDelivery.ONCHAIN_CONSTRAINED); - - self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round( - player, - game_id, - round, - )); - } - // docs:end:play-round - - #[external("public")] - #[only_self] - fn validate_and_play_round(player: AztecAddress, game_id: Field, round: u8) { - let game_in_progress = self.storage.races.at(game_id).read(); - self.storage.races.at(game_id).write(game_in_progress.increment_player_round(player, round)); - } - - // docs:start:finish-game - /// Reveals a player's total scores per track. - /// Reads private round notes and publishes aggregated totals. - #[external("private")] - fn finish_game(game_id: Field) { - let player = self.msg_sender(); - let totals = - self.storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new()); - - let mut total_track1: u64 = 0; - let mut total_track2: u64 = 0; - let mut total_track3: u64 = 0; - let mut total_track4: u64 = 0; - let mut total_track5: u64 = 0; - - for i in 0..TOTAL_ROUNDS { - total_track1 += totals.get(i as u32).note.track1 as u64; - total_track2 += totals.get(i as u32).note.track2 as u64; - total_track3 += totals.get(i as u32).note.track3 as u64; - total_track4 += totals.get(i as u32).note.track4 as u64; - total_track5 += totals.get(i as u32).note.track5 as u64; - } - - self.enqueue(PodRacing::at(self.context.this_address()).validate_finish_game_and_reveal( - player, - game_id, - total_track1, - total_track2, - total_track3, - total_track4, - total_track5, - )); - } - // docs:end:finish-game - - #[external("public")] - #[only_self] - fn validate_finish_game_and_reveal( - player: AztecAddress, - game_id: Field, - total_track1: u64, - total_track2: u64, - total_track3: u64, - total_track4: u64, - total_track5: u64, - ) { - let game_in_progress = self.storage.races.at(game_id).read(); - self.storage.races.at(game_id).write(game_in_progress.set_player_scores( - player, - total_track1, - total_track2, - total_track3, - total_track4, - total_track5, - )); - } - - // docs:start:finalize-game - /// Determines the winner after both players have revealed and the game has expired. - #[external("public")] - fn finalize_game(game_id: Field) { - let game_in_progress = self.storage.races.at(game_id).read(); - let winner = game_in_progress.calculate_winner(self.context.block_number()); - let previous_wins = self.storage.win_history.at(winner).read(); - self.storage.win_history.at(winner).write(previous_wins + 1); - } - // docs:end:finalize-game -} diff --git a/docs/examples/webapp-tutorial/src/App.tsx b/docs/examples/webapp-tutorial/src/App.tsx deleted file mode 100644 index 373b4ba55a18..000000000000 --- a/docs/examples/webapp-tutorial/src/App.tsx +++ /dev/null @@ -1,131 +0,0 @@ -// docs:start:app-imports -import React, { useState } from 'react'; -import type { Wallet } from '@aztec/aztec.js/wallet'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { NetworkType } from './config'; -import type { PodRacingContract } from './artifacts/PodRacing'; -import { NetworkPicker } from './components/NetworkPicker'; -import { WalletConnect } from './components/WalletConnect'; -import { AccountInfo } from './components/AccountInfo'; -import { GameLobby } from './components/GameLobby'; -import { GameBoard } from './components/GameBoard'; -import { GameStatus } from './components/GameStatus'; -import { ErrorBoundary } from './components/ErrorBoundary'; -import { LogProvider, TransactionLog } from './components/TransactionLog'; -import { TwoPlayerLocal } from './components/TwoPlayerLocal'; -import { EmbeddedWallet } from './embedded-wallet'; -// docs:end:app-imports - -// docs:start:app-state -type AppPhase = 'connect' | 'lobby' | 'playing'; - -function App() { - const [network, setNetwork] = useState('local'); - const [wallet, setWallet] = useState(null); - const [account, setAccount] = useState(null); - const [phase, setPhase] = useState('connect'); - const [contract, setContract] = useState(null); - const [gameId, setGameId] = useState(BigInt(0)); - const [currentRound, setCurrentRound] = useState(1); -// docs:end:app-state - - // docs:start:app-handlers - async function handleWalletConnected(w: Wallet | EmbeddedWallet) { - setWallet(w); - if (w instanceof EmbeddedWallet) { - setAccount(w.getConnectedAccount()); - setPhase('lobby'); - } else { - // Extension wallet — getAccounts returns the active account(s) - try { - const accounts = await w.getAccounts(); - console.log('Accounts received:', accounts); - if (accounts && accounts.length > 0) { - const addr = accounts[0].item; - console.log('Setting account:', addr); - setAccount(addr); - setPhase('lobby'); - } else { - alert('Please create an account in the wallet extension first, then refresh the page.'); - } - } catch (err: unknown) { - console.error('Error getting accounts:', err); - alert(`Error connecting to wallet: ${err}`); - } - } - } - - function handleGameJoined(c: PodRacingContract, gId: bigint) { - setContract(c); - setGameId(gId); - setCurrentRound(1); - setPhase('playing'); - } - - function handleRoundPlayed() { - setCurrentRound((r) => r + 1); - } - // docs:end:app-handlers - - // docs:start:app-render - return ( - - -
-
-

Pod Racing on Aztec

- - {network === 'remote' && account && } -
- -
- {/* Local network: Two-player split-screen mode */} - {network === 'local' && } - - {/* Remote: Single-player mode with wallet extension */} - {network === 'remote' && phase === 'connect' && ( - - )} - - {network === 'remote' && phase === 'lobby' && wallet && account && ( - - )} - - {network === 'remote' && phase === 'playing' && wallet && account && contract && ( -
- - -
- )} - - -
-
-
-
- ); - // docs:end:app-render -} - -export { App }; diff --git a/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx b/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx deleted file mode 100644 index 57deabff998d..000000000000 --- a/docs/examples/webapp-tutorial/src/components/ErrorBoundary.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -interface ErrorBoundaryState { - hasError: boolean; - error: Error | null; -} - -/** - * Catches unhandled errors during rendering and shows a fallback UI - * instead of crashing the entire application with a blank screen. - */ -export class ErrorBoundary extends React.Component< - { children: React.ReactNode }, - ErrorBoundaryState -> { - constructor(props: { children: React.ReactNode }) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error('ErrorBoundary caught:', error, errorInfo); - } - - render() { - if (this.state.hasError) { - return ( -
-

Something went wrong

-

The application encountered an unexpected error.

- - {this.state.error && ( -
{this.state.error.message}
- )} -
- ); - } - - return this.props.children; - } -} diff --git a/docs/examples/webapp-tutorial/src/components/GameBoard.tsx b/docs/examples/webapp-tutorial/src/components/GameBoard.tsx deleted file mode 100644 index 671d05d0cc6b..000000000000 --- a/docs/examples/webapp-tutorial/src/components/GameBoard.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useState } from 'react'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { PodRacingContract } from '../artifacts/PodRacing'; -import { playRound, finishGame, finalizeGame } from '../contract'; -import { TRACK_NAMES, MAX_POINTS_PER_ROUND, TOTAL_ROUNDS } from '../game-constants'; -import { useTransactionLog } from './TransactionLog'; - -interface GameBoardProps { - contract: PodRacingContract; - account: AztecAddress; - gameId: bigint; - currentRound: number; - onRoundPlayed: () => void; -} - -export function GameBoard({ - contract, - account, - gameId, - currentRound, - onRoundPlayed, -}: GameBoardProps) { - const [allocations, setAllocations] = useState<[number, number, number, number, number]>([2, 2, 2, 2, 1]); - const [loading, setLoading] = useState(false); - const [status, setStatus] = useState(''); - const { addLog } = useTransactionLog(); - - function updateAllocation(trackIndex: number, value: number) { - const newAllocations = [...allocations] as [number, number, number, number, number]; - newAllocations[trackIndex] = value; - setAllocations(newAllocations); - } - - const total = allocations.reduce((sum, v) => sum + v, 0); - - // docs:start:submit-round - async function handleSubmitRound() { - if (total >= 10) { - setStatus(`Points must sum to less than 10 (currently ${total})`); - return; - } - - setLoading(true); - setStatus('Submitting your allocation (private transaction)...'); - addLog(`Round ${currentRound}: Submitting allocation [${allocations.join(', ')}]...`, 'pending'); - try { - addLog('Building private transaction proof...', 'pending'); - const receipt = await playRound(contract, account, gameId, currentRound, allocations); - addLog(`Round ${currentRound} submitted successfully`, 'success', receipt.receipt.txHash?.toString()); - setStatus('Round submitted!'); - setAllocations([2, 2, 2, 2, 1]); - onRoundPlayed(); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Error submitting round: ${msg}`, 'error'); - } finally { - setLoading(false); - } - } - // docs:end:submit-round - - // docs:start:finish-and-finalize - async function handleFinishGame() { - setLoading(true); - setStatus('Revealing your total scores...'); - addLog('Revealing scores (finish_game)...', 'pending'); - try { - addLog('Reading private notes and computing totals...', 'pending'); - const receipt = await finishGame(contract, account, gameId); - addLog('Scores revealed successfully', 'success', receipt.receipt.txHash?.toString()); - setStatus('Scores revealed! Waiting for opponent to reveal, then finalize.'); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Error revealing scores: ${msg}`, 'error'); - } finally { - setLoading(false); - } - } - - async function handleFinalizeGame() { - setLoading(true); - setStatus('Determining winner...'); - addLog('Finalizing game and determining winner...', 'pending'); - try { - const receipt = await finalizeGame(contract, account, gameId); - addLog('Game finalized! Winner determined.', 'success', receipt.receipt.txHash?.toString()); - setStatus('Game finalized! Winner determined.'); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Error finalizing game: ${msg}`, 'error'); - } finally { - setLoading(false); - } - } - // docs:end:finish-and-finalize - - const allRoundsPlayed = currentRound > TOTAL_ROUNDS; - - return ( -
-

{allRoundsPlayed ? 'All Rounds Played' : `Round ${currentRound} of ${TOTAL_ROUNDS}`}

- {!allRoundsPlayed && ( -

Allocate up to {MAX_POINTS_PER_ROUND} points across 5 tracks. Your allocation is private.

- )} - {status &&

{status}

} - - {!allRoundsPlayed && ( - <> -
- {TRACK_NAMES.map((name, i) => ( -
- - updateAllocation(i, Number(e.target.value))} - disabled={loading} - /> - {allocations[i]} pts -
- ))} -
- -

= 10 ? 'error' : ''}`}> - Total: {total} / {MAX_POINTS_PER_ROUND} max -

- - - - )} - - {allRoundsPlayed && ( -
- - -
- )} -
- ); -} diff --git a/docs/examples/webapp-tutorial/src/components/GameLobby.tsx b/docs/examples/webapp-tutorial/src/components/GameLobby.tsx deleted file mode 100644 index 05af34336f25..000000000000 --- a/docs/examples/webapp-tutorial/src/components/GameLobby.tsx +++ /dev/null @@ -1,154 +0,0 @@ -// docs:start:game-lobby-imports -import React, { useState } from 'react'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { Wallet } from '@aztec/aztec.js/wallet'; -import type { PodRacingContract } from '../artifacts/PodRacing'; -import { deployContract, createGame, joinGame, attachToContract } from '../contract'; -import { useTransactionLog } from './TransactionLog'; - -interface GameLobbyProps { - wallet: Wallet; - account: AztecAddress; - onGameJoined: (contract: PodRacingContract, gameId: bigint) => void; -} -// docs:end:game-lobby-imports - -export function GameLobby({ wallet, account, onGameJoined }: GameLobbyProps) { - const [status, setStatus] = useState(''); - const [isCreating, setIsCreating] = useState(false); - const [isJoining, setIsJoining] = useState(false); - const [gameId, setGameId] = useState('1'); - const [joinGameIdInput, setJoinGameIdInput] = useState(''); - const [joinContractAddress, setJoinContractAddress] = useState(''); - const { addLog } = useTransactionLog(); - - // docs:start:handle-create - async function handleCreateGame() { - setIsCreating(true); - setStatus('Deploying Pod Racing contract...'); - addLog('Starting contract deployment...', 'pending'); - try { - let gId: bigint; - try { - gId = BigInt(gameId); - if (gId <= 0n) throw new Error('must be positive'); - } catch { - setStatus('Invalid game ID — enter a positive integer'); - setIsCreating(false); - return; - } - - addLog('Compiling and sending deployment transaction...', 'pending'); - const contract = await deployContract(wallet, account); - addLog(`Contract deployed at ${contract.address.toString()}`, 'success'); - - setStatus('Creating game...'); - addLog('Creating game...', 'pending'); - const receipt = await createGame(contract, account, gId); - addLog(`Game ${gId} created successfully`, 'success', receipt.receipt.txHash?.toString()); - - setStatus(`Game created! Share contract address: ${contract.address}`); - onGameJoined(contract, gId); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Error: ${msg}`, 'error'); - } finally { - setIsCreating(false); - } - } - // docs:end:handle-create - - // docs:start:handle-join - async function handleJoinGame() { - if (!joinContractAddress || !joinGameIdInput) { - setStatus('Enter contract address and game ID'); - return; - } - setIsJoining(true); - setStatus('Joining game...'); - addLog('Attaching to existing contract...', 'pending'); - try { - let gId: bigint; - try { - gId = BigInt(joinGameIdInput); - if (gId <= 0n) throw new Error('must be positive'); - } catch { - setStatus('Invalid game ID — enter a positive integer'); - setIsJoining(false); - return; - } - - const contractAddr = AztecAddress.fromString(joinContractAddress); - const contract = await attachToContract( - wallet, - contractAddr - ); - addLog(`Attached to contract ${contractAddr.toString()}`, 'info'); - addLog(`Joining game ${gId}...`, 'pending'); - const receipt = await joinGame(contract, account, gId); - addLog(`Joined game ${gId} successfully`, 'success', receipt.receipt.txHash?.toString()); - - setStatus('Joined game!'); - onGameJoined(contract, gId); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Error joining game: ${msg}`, 'error'); - } finally { - setIsJoining(false); - } - } - // docs:end:handle-join - - return ( -
-

Game Lobby

- {status &&

{status}

} - -
-

Create New Game

- - -
- -
-

Join Existing Game

- - - -
-
- ); -} diff --git a/docs/examples/webapp-tutorial/src/components/GameStatus.tsx b/docs/examples/webapp-tutorial/src/components/GameStatus.tsx deleted file mode 100644 index b0c652a645e3..000000000000 --- a/docs/examples/webapp-tutorial/src/components/GameStatus.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; - -interface GameStatusProps { - account: AztecAddress; - gameId: bigint; - currentRound: number; -} - -// docs:start:game-status-component -/** - * Displays the current game status. - * - * In the Pod Racing contract, round progress is tracked publicly - * (which round each player is on), but point allocations are private. - * The currentRound is tracked locally in React state and incremented - * after each successful play_round transaction. - */ -export function GameStatus({ account, gameId, currentRound }: GameStatusProps) { - const addr = account.toString(); - const display = `${addr.slice(0, 10)}...${addr.slice(-6)}`; - - return ( -
-

Game Status

-

Game ID: {gameId.toString()}

-

Playing as: {display}

-

Current Round: {currentRound} / 3

-

- Your point allocations are stored as private notes. - Opponents cannot see your strategy until you reveal scores. -

-
- ); -} -// docs:end:game-status-component diff --git a/docs/examples/webapp-tutorial/src/components/TxStatus.tsx b/docs/examples/webapp-tutorial/src/components/TxStatus.tsx deleted file mode 100644 index 060a436c52f1..000000000000 --- a/docs/examples/webapp-tutorial/src/components/TxStatus.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -type TxState = 'idle' | 'sending' | 'proving' | 'confirmed' | 'error'; - -interface TxStatusProps { - state: TxState; - txHash?: string; - error?: string; -} - -// docs:start:tx-status-component -/** - * Displays the current transaction lifecycle stage. - * Transactions flow: send() -> proving -> confirmed (or error). - */ -export function TxStatus({ state, txHash, error }: TxStatusProps) { - if (state === 'idle') return null; - - const messages: Record = { - idle: '', - sending: 'Sending transaction...', - proving: 'Proving transaction (generating ZK proof)...', - confirmed: 'Transaction confirmed!', - error: `Transaction failed: ${error}`, - }; - - return ( -
-

{messages[state]}

- {txHash &&

Tx: {txHash.slice(0, 10)}...

} -
- ); -} -// docs:end:tx-status-component diff --git a/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx b/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx deleted file mode 100644 index ddcf03d62743..000000000000 --- a/docs/examples/webapp-tutorial/src/components/WalletConnect.tsx +++ /dev/null @@ -1,198 +0,0 @@ -// docs:start:wallet-connect-imports -import React, { useState, useEffect } from 'react'; -import type { Wallet, GrantedAccountsCapability } from '@aztec/aztec.js/wallet'; -import type { WalletProvider } from '@aztec/wallet-sdk/manager'; -import type { NetworkType } from '../config'; -import { EmbeddedWallet } from '../embedded-wallet'; -import { discoverWallets, connectToProvider, getAppCapabilities } from '../wallet-connection'; -import { getNodeUrl } from '../config'; -import { useTransactionLog } from './TransactionLog'; - -interface WalletConnectProps { - network: NetworkType; - onWalletConnected: (wallet: Wallet | EmbeddedWallet) => void; -} -// docs:end:wallet-connect-imports - -// docs:start:wallet-connect-component -export function WalletConnect({ network, onWalletConnected }: WalletConnectProps) { - const [status, setStatus] = useState(''); - const [providers, setProviders] = useState([]); - const [verificationEmojis, setVerificationEmojis] = useState(null); - const [testAccountIndex, setTestAccountIndex] = useState(0); - const [loading, setLoading] = useState(false); - const [connected, setConnected] = useState(false); - const [discoveryDone, setDiscoveryDone] = useState(false); - const { addLog } = useTransactionLog(); - - /** Connect using the embedded wallet with a pre-deployed test account */ - async function connectLocal() { - setLoading(true); - setStatus('Initializing PXE (this may take a moment)...'); - addLog('Initializing local PXE client...', 'pending'); - try { - const nodeUrl = getNodeUrl('local'); - addLog(`Connecting to node at ${nodeUrl}`, 'info'); - const wallet = await EmbeddedWallet.initialize(nodeUrl); - addLog('PXE initialized successfully', 'success'); - - setStatus('Connecting test account...'); - addLog(`Connecting test account #${testAccountIndex + 1}...`, 'pending'); - await wallet.connectTestAccount(testAccountIndex); - addLog(`Test account #${testAccountIndex + 1} connected`, 'success'); - - setStatus('Connected!'); - onWalletConnected(wallet); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Connection error: ${msg}`, 'error'); - } finally { - setLoading(false); - } - } - - // docs:start:remote-connect - /** Discover and connect to a browser extension wallet */ - useEffect(() => { - if (network !== 'remote') return; - - setStatus('Discovering wallet extensions...'); - setDiscoveryDone(false); - const { cancel, done } = discoverWallets(31337, 'pod-racing', (found) => { - setProviders(found); - setStatus(`Found ${found.length} wallet(s)`); - }); - - let isMounted = true; - done.then(() => { - if (isMounted) setDiscoveryDone(true); - }).catch((err) => { - if (isMounted) setStatus(`Discovery error: ${err.message}`); - }); - - return () => { - isMounted = false; - cancel(); - }; - }, [network]); - - async function connectExtension(provider: WalletProvider) { - setLoading(true); - setStatus('Establishing secure channel...'); - try { - const { emojis, confirm } = await connectToProvider( - provider, - 'pod-racing' - ); - - // Show emojis for reference — the wallet extension is the authority - // that verifies the emojis match. confirm() is a local operation - // that creates the ExtensionWallet proxy (no message sent to extension). - setVerificationEmojis(emojis); - setStatus('Verify these emojis match in the wallet extension, then approve there.'); - - const wallet = await confirm(); - - // Request capabilities — the dApp declares all permissions it needs upfront. - // The extension shows an approval dialog; this call blocks until the user approves. - setStatus('Requesting permissions from wallet extension...'); - const manifest = getAppCapabilities(); - - const capabilities = await wallet.requestCapabilities(manifest); - setVerificationEmojis(null); - console.log('[WalletConnect] Granted capabilities:', capabilities); - - // Check if accounts were granted - const accountsCap = capabilities.granted.find( - (c): c is GrantedAccountsCapability => c.type === 'accounts' - ); - - if (!accountsCap?.accounts?.length) { - setStatus('No accounts granted. Please approve the capabilities request in the wallet extension.'); - setLoading(false); - return; - } - - setConnected(true); - setStatus('Connected!'); - addLog('Connected to extension wallet', 'success'); - onWalletConnected(wallet); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - setStatus(`Error: ${msg}`); - addLog(`Connection error: ${msg}`, 'error'); - } finally { - setLoading(false); - } - } - - // docs:end:remote-connect - - return ( -
-

Connect Wallet

- {status &&

{status}

} - - {network === 'local' && ( -
- - -
- )} - - {network === 'remote' && !verificationEmojis && !connected && ( -
- {providers.length === 0 && ( - discoveryDone - ?

No wallet extensions found. Install an Aztec wallet extension.

- :

Looking for wallet extensions...

- )} - {providers.map((provider, i) => ( - - ))} -
- )} - - {verificationEmojis && ( -
-

Verify Connection

-

Check that these emojis match what your wallet extension shows, then approve there:

-
{verificationEmojis}
-
- )} - -
- ); -} -// docs:end:wallet-connect-component diff --git a/docs/examples/webapp-tutorial/src/config.ts b/docs/examples/webapp-tutorial/src/config.ts deleted file mode 100644 index 423e7c4b613e..000000000000 --- a/docs/examples/webapp-tutorial/src/config.ts +++ /dev/null @@ -1,33 +0,0 @@ -// docs:start:config -import { createAztecNodeClient } from '@aztec/aztec.js/node'; -import { getPXEConfig } from '@aztec/pxe/config'; -import { createPXE } from '@aztec/pxe/client/lazy'; - - -export type NetworkType = 'local' | 'remote'; - -export function getNodeUrl(network: NetworkType): string { - if (network === 'local') { - return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; - } - // For remote networks, the wallet extension manages the node connection - return process.env.AZTEC_NODE_URL || 'http://localhost:8080'; -} - -/** - * Creates an in-browser PXE instance connected to an Aztec node. - * PXE (Private eXecution Environment) runs locally and handles - * private state, note discovery, and transaction creation. - */ -export async function createLocalPXE(nodeUrl: string) { - const aztecNode = createAztecNodeClient(nodeUrl); - const config = getPXEConfig(); - config.l1Contracts = await aztecNode.getL1ContractAddresses(); - const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); - config.proverEnabled = !isLocal; - const pxe = await createPXE(aztecNode, config, {}); - console.log('PXE connected to node at:', nodeUrl); - - return { pxe, aztecNode }; -} -// docs:end:config diff --git a/docs/examples/webapp-tutorial/src/contract.ts b/docs/examples/webapp-tutorial/src/contract.ts deleted file mode 100644 index 00b44c343300..000000000000 --- a/docs/examples/webapp-tutorial/src/contract.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { Wallet } from '@aztec/aztec.js/wallet'; -// @ts-ignore — generated artifact, may not exist until compiled -import { PodRacingContract, PodRacingContractArtifact } from './artifacts/PodRacing'; -import { createSponsoredFeePayment } from './fees'; -import { EmbeddedWallet } from './embedded-wallet'; - -/** - * Deploys a new Pod Racing contract. - * The deployer becomes the game admin. - */ -export async function deployContract(wallet: Wallet, deployer: AztecAddress): Promise { - const paymentMethod = await createSponsoredFeePayment(); - const { contract } = await PodRacingContract.deploy(wallet, deployer) - .send({ from: deployer, fee: { paymentMethod } }); - - console.log('Pod Racing contract deployed at:', contract.address.toString()); - return contract; -} - -/** - * Attaches to an existing deployed Pod Racing contract. - * Registers the contract with PXE so private functions can execute locally. - */ -export async function attachToContract( - wallet: Wallet, - contractAddress: AztecAddress -) { - if (wallet instanceof EmbeddedWallet) { - await wallet.registerContractFromNode(contractAddress, PodRacingContractArtifact); - } - return PodRacingContract.at(contractAddress, wallet); -} - -/** - * Creates a new game with the given game_id. - */ -export async function createGame( - contract: PodRacingContract, - from: AztecAddress, - gameId: bigint -) { - const paymentMethod = await createSponsoredFeePayment(); - const receipt = await contract.methods - .create_game(gameId) - .send({ from, fee: { paymentMethod } }); - - console.log('Game created, tx hash:', receipt.receipt.txHash.toString()); - return receipt; -} - -/** - * Joins an existing game as player2. - */ -export async function joinGame( - contract: PodRacingContract, - from: AztecAddress, - gameId: bigint -) { - const paymentMethod = await createSponsoredFeePayment(); - const receipt = await contract.methods - .join_game(gameId) - .send({ from, fee: { paymentMethod } }); - - console.log('Joined game, tx hash:', receipt.receipt.txHash.toString()); - return receipt; -} - -/** - * Allocates points to 5 tracks for a round (private transaction). - */ -export async function playRound( - contract: PodRacingContract, - from: AztecAddress, - gameId: bigint, - round: number, - tracks: [number, number, number, number, number] -) { - const paymentMethod = await createSponsoredFeePayment(); - const receipt = await contract.methods - .play_round(gameId, round, tracks[0], tracks[1], tracks[2], tracks[3], tracks[4]) - .send({ from, fee: { paymentMethod } }); - - console.log('Round played, tx hash:', receipt.receipt.txHash.toString()); - return receipt; -} - -/** - * Reveals your total scores per track after all rounds are played. - */ -export async function finishGame( - contract: PodRacingContract, - from: AztecAddress, - gameId: bigint -) { - const paymentMethod = await createSponsoredFeePayment(); - const receipt = await contract.methods - .finish_game(gameId) - .send({ from, fee: { paymentMethod } }); - - console.log('Game finished (scores revealed), tx hash:', receipt.receipt.txHash.toString()); - return receipt; -} - -/** - * Determines the winner after both players have revealed. - */ -export async function finalizeGame( - contract: PodRacingContract, - from: AztecAddress, - gameId: bigint -) { - const paymentMethod = await createSponsoredFeePayment(); - const receipt = await contract.methods - .finalize_game(gameId) - .send({ from, fee: { paymentMethod } }); - - console.log('Game finalized, tx hash:', receipt.receipt.txHash.toString()); - return receipt; -} diff --git a/docs/examples/webapp-tutorial/src/embedded-wallet.ts b/docs/examples/webapp-tutorial/src/embedded-wallet.ts deleted file mode 100644 index 969ccafd839e..000000000000 --- a/docs/examples/webapp-tutorial/src/embedded-wallet.ts +++ /dev/null @@ -1,144 +0,0 @@ -// docs:start:embedded-wallet-imports -import { type NoFrom, NO_FROM } 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 { SPONSORED_FPC_SALT } from '@aztec/constants'; -import { AccountFeePaymentMethodOptions } from '@aztec/entrypoints/account'; -import type { FieldsOf } from '@aztec/foundation/types'; -import { getInitialTestAccountsData } from '@aztec/accounts/testing/lazy'; -import type { ContractArtifact } from '@aztec/stdlib/abi'; -import { GasSettings } from '@aztec/stdlib/gas'; -import { type FeeOptions } from '@aztec/wallet-sdk/base-wallet'; -import { EmbeddedWallet as BaseEmbeddedWallet } from '@aztec/wallets/embedded'; -// docs:end:embedded-wallet-imports - -// docs:start:embedded-wallet-class -/** - * A tutorial wallet for local development. - * Extends the official EmbeddedWallet to add SponsoredFPC fee payment - * so users don't need to hold fee tokens. - * - * Inherits from the SDK's EmbeddedWallet which provides: - * - Account creation and persistence via WalletDB - * - Pre-simulation with gas estimation in sendTx - * - Automatic authwitness generation - * - Stub-account simulation (no expensive kernel proving) - */ -export class EmbeddedWallet extends BaseEmbeddedWallet { - connectedAccount: AztecAddress | null = null; - - // docs:start:fee-options - /** - * Uses SponsoredFPC for fee payment by default, so users - * don't need to hold fee tokens. - */ - override async completeFeeOptions( - from: AztecAddress | NoFrom, - feePayer?: AztecAddress, - gasSettings?: Partial>, - ): Promise { - const maxFeesPerGas = - gasSettings?.maxFeesPerGas ?? - (await this.aztecNode.getCurrentMinFees()).mul(1 + this.minFeePadding); - - let walletFeePaymentMethod; - let accountFeePaymentMethodOptions; - - if (!feePayer) { - const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); - walletFeePaymentMethod = new SponsoredFeePaymentMethod( - fpc.instance.address, - ); - if (from !== NO_FROM) { - accountFeePaymentMethodOptions = AccountFeePaymentMethodOptions.EXTERNAL; - } - } else if (from !== NO_FROM) { - accountFeePaymentMethodOptions = from.equals(feePayer) - ? AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM - : AccountFeePaymentMethodOptions.EXTERNAL; - } - - return { - gasSettings: GasSettings.default({ ...gasSettings, maxFeesPerGas }), - walletFeePaymentMethod, - accountFeePaymentMethodOptions, - }; - } - // docs:end:fee-options - - // docs:start:initialize - /** - * Creates a new EmbeddedWallet connected to the given Aztec node URL. - * Sets up an in-browser PXE and registers the SponsoredFPC contract. - */ - static async initialize(nodeUrl: string) { - const isLocal = - nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); - const wallet = await EmbeddedWallet.create(nodeUrl, { - ephemeral: true, - pxeConfig: { proverEnabled: !isLocal }, - }); - - // Register SponsoredFPC so we can pay fees - const fpc = await EmbeddedWallet.#getSponsoredFPCContract(); - await wallet.registerContract(fpc.instance, fpc.artifact); - - return wallet; - } - // docs:end:initialize - - static async #getSponsoredFPCContract() { - const { SponsoredFPCContractArtifact } = await import( - '@aztec/noir-contracts.js/SponsoredFPC' - ); - const instance = await getContractInstanceFromInstantiationParams( - SponsoredFPCContractArtifact, - { salt: new Fr(SPONSORED_FPC_SALT) }, - ); - return { instance, artifact: SponsoredFPCContractArtifact }; - } - - getConnectedAccount() { - return this.connectedAccount; - } - - // docs:start:connect-test-account - /** - * Connects one of the pre-deployed test accounts available on the local network. - * Uses the inherited createSchnorrAccount which handles account creation, - * contract registration, and WalletDB persistence. - */ - async connectTestAccount(index: number) { - const testAccounts = await getInitialTestAccountsData(); - const accountData = testAccounts[index]; - - const accountManager = await this.createSchnorrAccount( - accountData.secret, - accountData.salt, - accountData.signingKey, - ); - - this.connectedAccount = accountManager.address; - return this.connectedAccount; - } - // docs:end:connect-test-account - - /** - * Fetches a contract instance from the Aztec node (onchain) and registers it - * with this wallet's PXE. Required before calling private functions on contracts - * deployed by another wallet/PXE. - */ - async registerContractFromNode( - address: AztecAddress, - artifact: ContractArtifact, - ) { - const instance = await this.aztecNode.getContract(address); - if (!instance) { - throw new Error(`Contract not found onchain at ${address}`); - } - await this.registerContract(instance, artifact); - } - // docs:end:embedded-wallet-class -} diff --git a/docs/examples/webapp-tutorial/src/fees.ts b/docs/examples/webapp-tutorial/src/fees.ts deleted file mode 100644 index 98b85724821d..000000000000 --- a/docs/examples/webapp-tutorial/src/fees.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 { SPONSORED_FPC_SALT } from '@aztec/constants'; -import type { PXE } from '@aztec/pxe/server'; - -// docs:start:get-sponsored-fpc -/** - * Returns the SponsoredFPC contract details. - * The SponsoredFPC (Fee Payment Contract) pays transaction fees on behalf of users. - * This is deployed at a well-known address derived from a fixed salt. - */ -export async function getSponsoredFPCContract() { - const { SponsoredFPCContractArtifact } = await import( - '@aztec/noir-contracts.js/SponsoredFPC' - ); - const instance = await getContractInstanceFromInstantiationParams( - SponsoredFPCContractArtifact, - { salt: new Fr(SPONSORED_FPC_SALT) } - ); - return { instance, artifact: SponsoredFPCContractArtifact }; -} -// docs:end:get-sponsored-fpc - -// docs:start:register-fpc -/** - * Registers the SponsoredFPC contract with PXE so it can be used for fee payment. - * This must be called before sending any transactions. - */ -export async function registerSponsoredFPC(pxe: PXE) { - const contract = await getSponsoredFPCContract(); - await pxe.registerContract(contract); - return contract.instance.address; -} -// docs:end:register-fpc - -// docs:start:create-fee-payment -/** - * Creates a SponsoredFeePaymentMethod that can be passed as the - * `paymentMethod` option when sending transactions. - */ -export async function createSponsoredFeePayment() { - const contract = await getSponsoredFPCContract(); - return new SponsoredFeePaymentMethod(contract.instance.address); -} -// docs:end:create-fee-payment diff --git a/docs/examples/webapp-tutorial/src/game-constants.ts b/docs/examples/webapp-tutorial/src/game-constants.ts deleted file mode 100644 index 4e1ab1d2854a..000000000000 --- a/docs/examples/webapp-tutorial/src/game-constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Shared game constants used by all game UI components. */ - -export const TRACK_NAMES = ['Straight', 'Canyon', 'Asteroid Field', 'Nebula', 'Wormhole'] as const; -export const MAX_POINTS_PER_ROUND = 9; -export const TOTAL_ROUNDS = 3; diff --git a/docs/examples/webapp-tutorial/src/wallet-connection.ts b/docs/examples/webapp-tutorial/src/wallet-connection.ts deleted file mode 100644 index b279872be5ce..000000000000 --- a/docs/examples/webapp-tutorial/src/wallet-connection.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Fr } from '@aztec/aztec.js/fields'; -import type { Wallet, AppCapabilities } from '@aztec/aztec.js/wallet'; -import { WalletManager, type WalletProvider } from '@aztec/wallet-sdk/manager'; -import { hashToEmoji } from '@aztec/wallet-sdk/crypto'; - -// docs:start:wallet-sdk-types -export interface WalletDiscoveryState { - providers: WalletProvider[]; - selectedProvider: WalletProvider | null; - verificationEmojis: string | null; - wallet: Wallet | null; - status: 'idle' | 'discovering' | 'verifying' | 'connected' | 'error'; - error: string | null; -} - -export const initialWalletState: WalletDiscoveryState = { - providers: [], - selectedProvider: null, - verificationEmojis: null, - wallet: null, - status: 'idle', - error: null, -}; -// docs:end:wallet-sdk-types - -// docs:start:discover-wallets -/** - * Starts discovering available wallet extensions. - * Wallet extensions broadcast their availability via window.postMessage. - * Returns a cancel function and calls onUpdate with each discovered wallet. - */ -export function discoverWallets( - chainId: number, - appId: string, - onUpdate: (providers: WalletProvider[]) => void -): { cancel: () => void; done: Promise } { - const manager = WalletManager.configure({ - extensions: { enabled: true }, - }); - - const providers: WalletProvider[] = []; - - const discovery = manager.getAvailableWallets({ - chainInfo: { - chainId: new Fr(chainId), - version: new Fr(1), - }, - appId, - onWalletDiscovered: (provider) => { - // Deduplicate by wallet ID (StrictMode or remounts can cause duplicate discoveries) - if (providers.some(p => p.id === provider.id)) { - return; - } - providers.push(provider); - onUpdate([...providers]); - }, - }); - - return { - cancel: () => discovery.cancel(), - done: discovery.done, - }; -} -// docs:end:discover-wallets - -// docs:start:connect-wallet -/** - * Connects to a discovered wallet provider. - * This establishes a secure encrypted channel using ECDH key exchange. - * The returned emojis should be shown to the user for verification. - */ -export async function connectToProvider( - provider: WalletProvider, - appId: string -): Promise<{ - emojis: string; - confirm: () => Promise; - cancel: () => void; -}> { - console.log('[wallet-connection] Calling establishSecureChannel for provider:', provider.name); - const pending = await provider.establishSecureChannel(appId); - console.log('[wallet-connection] Secure channel established, verificationHash:', pending.verificationHash); - const emojis = hashToEmoji(pending.verificationHash); - console.log('[wallet-connection] Emojis:', emojis); - - return { - emojis, - confirm: () => pending.confirm(), - cancel: () => pending.cancel(), - }; -} -// docs:end:connect-wallet - -// docs:start:app-capabilities -export function getAppCapabilities(): AppCapabilities { - return { - version: '1.0', - metadata: { - name: 'Pod Racing', - version: '1.0.0', - description: 'Pod Racing game on Aztec', - url: window.location.origin, - }, - capabilities: [ - { type: 'accounts', canGet: true }, - { type: 'contracts', contracts: '*', canRegister: true, canGetMetadata: true }, - { type: 'simulation', transactions: { scope: '*' }, utilities: { scope: '*' } }, - { type: 'transaction', scope: '*' }, - ], - }; -} -// docs:end:app-capabilities diff --git a/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts b/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts deleted file mode 100644 index 5aaf733f8bb6..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/account-utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Shared account contract instantiation logic. - * - * Centralizes the key derivation + contract setup sequence used by - * account creation, deployment, and registration. - */ - -import { getAztecCore } from './aztec-imports'; - -/** - * Derives keys and instantiates a Schnorr account contract from a secret and salt. - * - * Returns everything needed for PXE registration and deployment: - * - secretFr / saltFr — field element versions of the inputs - * - publicKeys — derived public keys - * - signingKey — Schnorr signing key - * - accountContract — the SchnorrAccountContract instance - * - artifact — the contract artifact - * - instance — the contract instance with computed address - */ -export async function instantiateAccount(secret: string, salt: string) { - const { - Fr, - deriveKeys, - deriveSigningKey, - SchnorrAccountContract, - getContractInstanceFromInstantiationParams, - } = await getAztecCore(); - - const secretFr = Fr.fromString(secret); - const saltFr = Fr.fromString(salt); - - const { publicKeys } = await deriveKeys(secretFr); - const signingKey = deriveSigningKey(secretFr); - const accountContract = new SchnorrAccountContract(signingKey); - - const initInfo = await accountContract.getInitializationFunctionAndArgs(); - const { constructorName, constructorArgs } = initInfo ?? { - constructorName: undefined, - constructorArgs: undefined, - }; - const artifact = await accountContract.getContractArtifact(); - - const instance = await getContractInstanceFromInstantiationParams(artifact, { - constructorArtifact: constructorName, - constructorArgs, - salt: saltFr, - publicKeys, - }); - - return { - secretFr, - saltFr, - publicKeys, - signingKey, - accountContract, - artifact, - instance, - }; -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts b/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts deleted file mode 100644 index 99c8795c1029..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/aztec-imports.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Centralized lazy import cache for Aztec SDK modules. - * - * The offscreen document uses dynamic imports to keep startup fast — we don't - * want to load Barretenberg, Schnorr account contracts, etc. until the user - * actually triggers an operation. But the same imports were scattered across - * 5+ locations with no shared cache. - * - * This module loads all needed Aztec imports once, caches them, and provides - * a typed interface for consumers. The module system caches the underlying - * import() calls, but this provides a single entry point and avoids repeating - * the destructuring boilerplate. - */ - -/** Core imports needed for account operations (key derivation, contract setup) */ -export interface AztecCoreImports { - Fr: typeof import('@aztec/aztec.js/fields').Fr; - AztecAddress: typeof import('@aztec/aztec.js/addresses').AztecAddress; - deriveKeys: typeof import('@aztec/stdlib/keys').deriveKeys; - deriveSigningKey: typeof import('@aztec/stdlib/keys').deriveSigningKey; - SchnorrAccountContract: typeof import('@aztec/accounts/schnorr/lazy').SchnorrAccountContract; - getContractInstanceFromInstantiationParams: typeof import('@aztec/aztec.js/contracts').getContractInstanceFromInstantiationParams; - AccountManager: typeof import('@aztec/aztec.js/wallet').AccountManager; -} - -/** Additional imports for the wallet runtime (BaseWallet, serialization) */ -export interface AztecWalletImports extends AztecCoreImports { - BaseWallet: typeof import('@aztec/wallet-sdk/base-wallet').BaseWallet; - SignerlessAccount: typeof import('@aztec/aztec.js/account').SignerlessAccount; - WalletSchema: typeof import('@aztec/aztec.js/wallet').WalletSchema; - jsonStringify: typeof import('@aztec/foundation/json-rpc').jsonStringify; - schemaHasMethod: typeof import('@aztec/foundation/schemas').schemaHasMethod; -} - -/** Deploy-specific imports (fee payment, SponsoredFPC) */ -export interface AztecDeployImports extends AztecCoreImports { - SponsoredFeePaymentMethod: typeof import('@aztec/aztec.js/fee').SponsoredFeePaymentMethod; - SponsoredFPCContract: typeof import('@aztec/noir-contracts.js/SponsoredFPC').SponsoredFPCContract; - SPONSORED_FPC_SALT: typeof import('@aztec/constants').SPONSORED_FPC_SALT; -} - -let coreCache: AztecCoreImports | null = null; -let walletCache: AztecWalletImports | null = null; -let deployCache: AztecDeployImports | null = null; - -/** - * Loads the core Aztec imports needed for account operations. - * Cached after first call. - */ -export async function getAztecCore(): Promise { - if (coreCache) return coreCache; - - const [fields, addresses, keys, schnorr, contracts, wallet] = await Promise.all([ - import('@aztec/aztec.js/fields'), - import('@aztec/aztec.js/addresses'), - import('@aztec/stdlib/keys'), - import('@aztec/accounts/schnorr/lazy'), - import('@aztec/aztec.js/contracts'), - import('@aztec/aztec.js/wallet'), - ]); - - coreCache = { - Fr: fields.Fr, - AztecAddress: addresses.AztecAddress, - deriveKeys: keys.deriveKeys, - deriveSigningKey: keys.deriveSigningKey, - SchnorrAccountContract: schnorr.SchnorrAccountContract, - getContractInstanceFromInstantiationParams: contracts.getContractInstanceFromInstantiationParams, - AccountManager: wallet.AccountManager, - }; - - return coreCache; -} - -/** - * Loads the wallet runtime imports (core + BaseWallet, serialization). - * Cached after first call. - */ -export async function getAztecWallet(): Promise { - if (walletCache) return walletCache; - - const [core, bw, account, walletMod, jsonRpc, schemas] = await Promise.all([ - getAztecCore(), - import('@aztec/wallet-sdk/base-wallet'), - import('@aztec/aztec.js/account'), - import('@aztec/aztec.js/wallet'), - import('@aztec/foundation/json-rpc'), - import('@aztec/foundation/schemas'), - ]); - - walletCache = { - ...core, - BaseWallet: bw.BaseWallet, - SignerlessAccount: account.SignerlessAccount, - WalletSchema: walletMod.WalletSchema, - jsonStringify: jsonRpc.jsonStringify, - schemaHasMethod: schemas.schemaHasMethod, - }; - - return walletCache; -} - -/** - * Loads the deploy-specific imports (core + fee payment, SponsoredFPC). - * Cached after first call. - */ -export async function getAztecDeploy(): Promise { - if (deployCache) return deployCache; - - const [core, fee, sponsoredFpc, constants] = await Promise.all([ - getAztecCore(), - import('@aztec/aztec.js/fee'), - import('@aztec/noir-contracts.js/SponsoredFPC'), - import('@aztec/constants'), - ]); - - deployCache = { - ...core, - SponsoredFeePaymentMethod: fee.SponsoredFeePaymentMethod, - SponsoredFPCContract: sponsoredFpc.SponsoredFPCContract, - SPONSORED_FPC_SALT: constants.SPONSORED_FPC_SALT, - }; - - return deployCache; -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/background.ts b/docs/examples/webapp-tutorial/test-extension/src/background.ts deleted file mode 100644 index e3c1a3342aed..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/background.ts +++ /dev/null @@ -1,1236 +0,0 @@ -/** - * Background service worker for the Aztec Tutorial Wallet. - * - * Handles: - * - Wallet SDK protocol (discovery, key exchange, encrypted wallet method calls) - * - Offscreen document lifecycle management (with retry on teardown) (#15) - * - Message routing between content scripts and offscreen document - * - Background task tracking with push notifications to popup via ports - * - State persistence via chrome.storage.session (survives SW restart) (#8) - * - Auto-lock via chrome.alarms (#28) - */ - -import { - BackgroundConnectionHandler, - type BackgroundTransport, - type BackgroundConnectionCallbacks, - type ActiveSession, -} from '@aztec/wallet-sdk/extension/handlers'; - -import { WALLET_CONFIG, MessageTarget, MessageTypes, AUTO_LOCK_MINUTES, log } from './config'; -import { getErrorMessage } from './utils'; -import { STORAGE_KEYS } from './wallet/storage'; -import type { PendingTransaction, PendingSessionVerification, PendingCapabilities, BackgroundTask } from './shared-types'; - -// docs:start:offscreen-management -let offscreenCreating: Promise | null = null; - -/** - * Ensures the offscreen document exists. Creates it if needed. - * The offscreen document hosts the PXE and wallet implementation. - */ -async function ensureOffscreenDocument(): Promise { - const existingContexts = await chrome.runtime.getContexts({ - contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], - }); - - if (existingContexts.length > 0) { - return; - } - - if (offscreenCreating) { - await offscreenCreating; - return; - } - - const offscreenUrl = chrome.runtime.getURL('dist/offscreen.html'); - log.debug('[background] Creating offscreen document:', offscreenUrl); - - offscreenCreating = chrome.offscreen.createDocument({ - url: offscreenUrl, - reasons: [chrome.offscreen.Reason.WORKERS], - justification: 'Aztec PXE requires long-running WASM operations', - }); - - await offscreenCreating; - offscreenCreating = null; - log.debug('[background] Offscreen document created'); -} -// docs:end:offscreen-management - -// docs:start:send-to-offscreen -/** - * Persistent port to the offscreen document. - * Unlike chrome.runtime.sendMessage() (broadcast), a port gives us: - * - Point-to-point channel (no broadcast to all extension pages) - * - Automatic disconnect detection (offscreen teardown) - * - No `return true`/`false` landmine for async responses - */ -let offscreenPort: chrome.runtime.Port | null = null; -const pendingOffscreenCalls = new Map void; - reject: (error: Error) => void; - timer: ReturnType; -}>(); -let offscreenMessageId = 0; - -const OFFSCREEN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes - -function connectOffscreenPort() { - const port = chrome.runtime.connect({ name: 'offscreen' }); - offscreenPort = port; - - port.onMessage.addListener((message: any) => { - // Progress updates — relay to popup - if (message.type === 'task-progress') { - const runningTask = backgroundTasks.find((t) => t.status === 'running'); - if (runningTask) { - runningTask.progress = message.stage; - notifyPopup({ type: 'task-update', task: { ...runningTask } }); - } - return; - } - - // Request/response correlation - const pending = pendingOffscreenCalls.get(message.messageId); - if (!pending) return; - pendingOffscreenCalls.delete(message.messageId); - clearTimeout(pending.timer); - - if (message.success) { - pending.resolve(message.result); - } else { - pending.reject(new Error(message.error || 'Unknown error')); - } - }); - - port.onDisconnect.addListener(() => { - log.debug('[background] Offscreen port disconnected'); - offscreenPort = null; - // Reject all pending calls — sendToOffscreen will retry - for (const [id, pending] of pendingOffscreenCalls) { - clearTimeout(pending.timer); - pending.reject(new Error('Offscreen port disconnected')); - pendingOffscreenCalls.delete(id); - } - }); -} - -/** - * Sends a message to the offscreen document and waits for response. - * Uses a persistent port with request/response correlation via messageId. - * Retries once if the offscreen document was torn down. (#15) - */ -async function sendToOffscreen(message: any, _retried = false): Promise { - await ensureOffscreenDocument(); - - if (!offscreenPort) { - connectOffscreenPort(); - } - - const messageId = `off-${++offscreenMessageId}`; - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - pendingOffscreenCalls.delete(messageId); - reject(new Error(`Offscreen call timed out: ${message.type}`)); - }, OFFSCREEN_TIMEOUT_MS); - - pendingOffscreenCalls.set(messageId, { resolve, reject, timer }); - - try { - if (!offscreenPort) { - throw new Error('Offscreen port not connected'); - } - offscreenPort.postMessage({ ...message, messageId }); - } catch (err: unknown) { - pendingOffscreenCalls.delete(messageId); - clearTimeout(timer); - - // Port may have disconnected — retry once - if (!_retried) { - log.warn('[background] Offscreen port send failed, retrying...'); - offscreenPort = null; - offscreenCreating = null; - sendToOffscreen(message, true).then(resolve, reject); - } else { - reject(err instanceof Error ? err : new Error(String(err))); - } - } - }); -} -// docs:end:send-to-offscreen - -// Store pending transactions, session verifications, and capability requests. -// Discovery and session tracking uses only the SDK handler. -let pendingTransactions: PendingTransaction[] = []; -let pendingSessionVerifications: PendingSessionVerification[] = []; -let pendingCapabilities: PendingCapabilities[] = []; -let walletUnlocked = false; - -/** Sessions that have had capabilities approved. Methods are blocked until this is set. */ -const capabilitiesApprovedSessions = new Set(); - -/** - * Messages queued while awaiting emoji verification. - * The extension must confirm the session before any wallet method calls are processed. - * This prevents the dApp from bypassing verification by sending messages immediately. - */ -const queuedMessages: Map = new Map(); - -/** - * Trusted origins persistence for auto-reconnect. (#30) - * - * When a user confirms emoji verification for a dApp, we remember the - * origin+appId in chrome.storage.local. On subsequent page refreshes, - * discovery and emoji verification are auto-approved (a fresh ECDH key - * exchange still happens every time for security). Disconnecting a site - * removes it from the trusted list. - */ -const TRUSTED_ORIGINS_KEY = 'aztec_trusted_origins'; - -interface TrustedOrigin { - origin: string; - appId: string; - trustedAt: number; - grantedCapabilities?: Array<{ type: string; [key: string]: any }>; -} - -async function getTrustedOrigins(): Promise { - const data = await chrome.storage.local.get(TRUSTED_ORIGINS_KEY); - return data[TRUSTED_ORIGINS_KEY] || []; -} - -async function addTrustedOrigin(origin: string, appId: string): Promise { - const trusted = await getTrustedOrigins(); - if (!trusted.some(t => t.origin === origin && t.appId === appId)) { - trusted.push({ origin, appId, trustedAt: Date.now() }); - await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: trusted }); - } -} - -async function removeTrustedOrigin(origin: string, appId: string): Promise { - const trusted = await getTrustedOrigins(); - const filtered = trusted.filter(t => !(t.origin === origin && t.appId === appId)); - await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: filtered }); -} - -async function isTrustedOrigin(origin: string, appId: string): Promise { - const trusted = await getTrustedOrigins(); - return trusted.some(t => t.origin === origin && t.appId === appId); -} - -async function getStoredCapabilities(origin: string, appId: string): Promise | null> { - const trusted = await getTrustedOrigins(); - const entry = trusted.find(t => t.origin === origin && t.appId === appId); - return entry?.grantedCapabilities ?? null; -} - -/** - * Persists critical state to chrome.storage.session. (#8) - * - * chrome.storage.session is: - * - Encrypted at rest - * - Scoped to the browser session (cleared when browser closes) - * - Survives service worker restarts (unlike in-memory variables) - * - * We persist: walletUnlocked, pendingTransactions. - * We do NOT persist: CryptoKey (lives in offscreen), active SDK sessions - * (the SDK handler can't be serialized; dApps must reconnect after SW restart). - */ -async function persistState(): Promise { - try { - await chrome.storage.session.set({ - sw_walletUnlocked: walletUnlocked, - sw_pendingTransactions: pendingTransactions, - }); - } catch (err) { - log.warn('[background] Failed to persist state:', err); - } -} - -async function restoreState(): Promise { - try { - const data = await chrome.storage.session.get([ - 'sw_walletUnlocked', - 'sw_pendingTransactions', - ]); - walletUnlocked = data.sw_walletUnlocked ?? false; - pendingTransactions = data.sw_pendingTransactions ?? []; - log.debug('[background] Restored state: unlocked =', walletUnlocked, ', pendingTx =', pendingTransactions.length); - - // Validate: if the offscreen document was torn down, the cached master key is gone. - // Probe offscreen to confirm — if it fails, the wallet needs re-unlock. - if (walletUnlocked) { - try { - await sendToOffscreen({ type: MessageTypes.GET_ACCOUNTS }); - } catch { - log.warn('[background] Offscreen unreachable after restore — marking wallet as locked'); - walletUnlocked = false; - await persistState(); - } - } - } catch (err) { - log.warn('[background] Failed to restore state:', err); - } -} - -/** - * Background task tracker for long-running operations. - * Tasks survive popup close/reopen. The popup receives real-time updates - * via a persistent port connection. - */ -let backgroundTasks: BackgroundTask[] = []; - -function startBackgroundTask(type: string, promise: Promise): string { - const id = `${type}-${Date.now()}`; - const task: BackgroundTask = { id, type, status: 'running', startedAt: Date.now() }; - backgroundTasks.push(task); - - promise - .then((result) => { - task.status = 'success'; - task.result = result; - notifyPopup({ type: 'task-update', task: { ...task } }); - }) - .catch((error) => { - task.status = 'error'; - task.error = getErrorMessage(error); - notifyPopup({ type: 'task-update', task: { ...task } }); - }); - - notifyPopup({ type: 'task-update', task: { ...task } }); - return id; -} - -/** - * Cleans up completed tasks after 5 minutes. (#13) - * 5 minutes gives the popup time to see completed tasks even if it was closed briefly. - */ -function cleanupTasks() { - const FIVE_MINUTES = 5 * 60 * 1000; - const cutoff = Date.now() - FIVE_MINUTES; - backgroundTasks = backgroundTasks.filter( - (t) => t.status === 'running' || t.startedAt > cutoff - ); -} - -/** - * Persistent port connection to the popup. - * Allows the background to push real-time updates without polling. - * The port disconnects automatically when the popup closes. - */ -let popupPort: chrome.runtime.Port | null = null; - -chrome.runtime.onConnect.addListener((port) => { - if (port.name !== 'popup') return; - - log.debug('[background] Popup connected'); - popupPort = port; - - port.onDisconnect.addListener(() => { - log.debug('[background] Popup disconnected'); - popupPort = null; - }); - - // Send current state immediately on connect - pushStateToPopup(); -}); - -function notifyPopup(message: any) { - if (popupPort) { - try { - popupPort.postMessage(message); - } catch { - popupPort = null; - } - } -} - -function pushStateToPopup() { - notifyPopup({ type: 'state', data: getFullState() }); -} - -/** - * Opens the popup via chrome.windows.create() — works without a user gesture. - * Used for discovery requests which are triggered by content script messages - * where chrome.action.openPopup() silently no-ops. - */ -function openPopupWindow() { - if (popupPort) { - pushStateToPopup(); - return; - } - chrome.windows.create({ - url: chrome.runtime.getURL('popup/popup.html'), - type: 'popup', - width: 400, - height: 600, - focused: true, - }).catch((err) => { - log.error('[background] Failed to create popup window:', err); - }); -} - -/** - * Opens the popup via chrome.action.openPopup() with windows.create() fallback. - * Used for flows that originate from a user gesture in the popup (tx approval, - * session verification) where openPopup() works reliably. - */ -function openPopupWithFallback() { - if (popupPort) { - pushStateToPopup(); - return; - } - chrome.action.openPopup().catch(() => { - chrome.windows.create({ - url: chrome.runtime.getURL('popup/popup.html'), - type: 'popup', - width: 400, - height: 600, - focused: true, - }); - }); -} - -function getFullState() { - cleanupTasks(); - const connectedSites = handler.getActiveSessions().map((s) => ({ - sessionId: s.sessionId, - origin: s.origin, - appId: s.appId, - connectedAt: s.connectedAt, - })); - - return { - discoveries: handler.getPendingDiscoveries(), - transactions: pendingTransactions, - pendingSessionVerifications, - pendingCapabilities, - connectedSites, - tasks: backgroundTasks, - }; -} - -/** - * Auto-lock via chrome.alarms. (#28) - * Resets the timer on every popup interaction. - */ -const AUTO_LOCK_ALARM = 'aztec-auto-lock'; - -function resetAutoLockTimer() { - if (walletUnlocked) { - chrome.alarms.create(AUTO_LOCK_ALARM, { delayInMinutes: AUTO_LOCK_MINUTES }); - } -} - -chrome.alarms.onAlarm.addListener(async (alarm) => { - if (alarm.name === AUTO_LOCK_ALARM) { - log.debug('[background] Auto-lock triggered'); - walletUnlocked = false; - await persistState(); - // Tell offscreen to clear the cached CryptoKey - try { - await sendToOffscreen({ type: MessageTypes.LOCK_WALLET }); - } catch { - // Offscreen may not exist yet - } - pushStateToPopup(); - } -}); - -/** - * Updates the extension badge to show pending items count. - */ -function updateBadge() { - const count = pendingTransactions.length + handler.getPendingDiscoveryCount() + pendingSessionVerifications.length + pendingCapabilities.length; - chrome.action.setBadgeText({ text: count > 0 ? count.toString() : '' }); - chrome.action.setBadgeBackgroundColor({ color: '#FF6B00' }); - pushStateToPopup(); - persistState(); -} - -// docs:start:transport -const transport: BackgroundTransport = { - sendToTab: (tabId, message) => { - log.debug('[background] sendToTab:', tabId, message.type, message.sessionId); - chrome.tabs.sendMessage(tabId, message); - }, - addContentListener: (handler) => { - chrome.runtime.onMessage.addListener((message, sender) => { - // Skip targeted messages (popup, offscreen), storage proxy, and progress updates - if (message.target) return; - if (message.type === 'storage-get' || message.type === 'storage-set') return; - - log.debug('[background] Content message received:', message.origin, message.type, 'from tab:', sender.tab?.id); - handler(message, { - tab: sender.tab ? { id: sender.tab.id, url: sender.tab.url } : undefined, - }); - }); - }, -}; -// docs:end:transport - -/** - * Returns the list of accounts grantable to dApps — only the active account, - * or the first account as fallback. Used by both processWalletMessage (for - * getAccounts/getRegisteredAccounts filtering) and APPROVE_CAPABILITIES. - */ -async function getGrantableAccounts(): Promise> { - const [accountsData, activeData] = await Promise.all([ - chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS), - chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT), - ]); - const allAccounts = accountsData[STORAGE_KEYS.ACCOUNTS] || []; - const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; - const activeAccount = allAccounts.find((a: any) => a.address === activeAddress); - if (activeAccount) { - return [{ alias: activeAccount.alias, item: activeAccount.address }]; - } - return allAccounts.slice(0, 1).map((a: any) => ({ alias: a.alias, item: a.address })); -} - -/** - * Processes a wallet method call from the ExtensionWallet proxy. - * Extracted so it can be called from onWalletMessage and when flushing the queue. - */ -async function processWalletMessage(session: ActiveSession, message: any) { - // Allow requestCapabilities to pass through (it's the mechanism for getting approved). - // Block all other wallet methods until capabilities have been approved for this session. - if (message.type !== 'requestCapabilities' && !capabilitiesApprovedSessions.has(session.sessionId)) { - log.warn('[background] Rejecting wallet method before capabilities approved:', message.type); - await handler.sendResponse(session.sessionId, { - messageId: message.messageId, - error: 'Capabilities not yet approved. Call requestCapabilities() first.', - walletId: WALLET_CONFIG.walletId, - }); - return; - } - - // docs:start:approval-check - // sendTx always requires approval — it's a state-changing operation. - // batch requires approval only if it contains a sendTx (e.g. BatchCall.send()). - // Read-only batches (simulateTx, executeUtility, etc.) auto-execute. - const needsApproval = - message.type === 'sendTx' || - (message.type === 'batch' && - Array.isArray(message.args?.[0]) && - message.args[0].some((m: any) => m.name === 'sendTx')); - - if (needsApproval) { - // Extract `from` address from the method args: - // - sendTx args: [executionPayload, sendOptions] → from is in sendOptions - // - batch args: [methodsArray] → find the sendTx entry and get from from its opts - let from = ''; - if (message.type === 'sendTx') { - from = message.args?.[1]?.from?.toString?.() || ''; - } else if (message.type === 'batch') { - const sendTxMethod = message.args[0].find((m: any) => m.name === 'sendTx'); - from = sendTxMethod?.args?.[1]?.from?.toString?.() || ''; - } - - const pending: PendingTransaction = { - sessionId: session.sessionId, - messageId: message.messageId, - method: message.type, - args: message.args, - from, - origin: session.origin, - timestamp: Date.now(), - }; - - pendingTransactions.push(pending); - updateBadge(); - log.debug('[background] Transaction pending approval:', pending.method); - - openPopupWithFallback(); - return; - } - // docs:end:approval-check - - // Capability requests require user approval — push to pending state. - if (message.type === 'requestCapabilities') { - // Auto-approve for trusted origins if the requested capabilities match what was previously granted - const requestedManifest = message.args?.[0]; - const requestedCaps: any[] = requestedManifest?.capabilities || []; - if (await isTrustedOrigin(session.origin, session.appId)) { - const savedCaps = await getStoredCapabilities(session.origin, session.appId); - if (savedCaps) { - // Verify the requested capability types match the previously approved set - const requestedTypes = requestedCaps.map((c: any) => c.type).sort(); - const savedTypes = savedCaps.map((c: any) => c.type).sort(); - const capsMatch = requestedTypes.length === savedTypes.length && - requestedTypes.every((t: string, i: number) => t === savedTypes[i]); - - if (capsMatch) { - const grantedAccounts = await getGrantableAccounts(); - const granted = savedCaps.map((cap: any) => { - if (cap.type === 'accounts') { - return { ...cap, accounts: grantedAccounts }; - } - return { ...cap }; - }); - - await handler.sendResponse(session.sessionId, { - messageId: message.messageId, - result: { - version: '1.0', - granted, - wallet: { name: WALLET_CONFIG.walletName, version: WALLET_CONFIG.walletVersion }, - }, - walletId: WALLET_CONFIG.walletId, - }); - capabilitiesApprovedSessions.add(session.sessionId); - log.debug('[background] Auto-approved capabilities for trusted origin:', session.origin); - return; - } - log.debug('[background] Requested capabilities differ from saved — requiring approval'); - } - } - - // Not trusted, no saved caps, or requested caps differ — require user approval - const manifest = requestedManifest; - const pending: PendingCapabilities = { - sessionId: session.sessionId, - messageId: message.messageId, - origin: session.origin, - appMetadata: manifest?.metadata || { name: 'Unknown App', version: '0.0.0' }, - capabilities: requestedCaps, - timestamp: Date.now(), - }; - pendingCapabilities.push(pending); - updateBadge(); - openPopupWithFallback(); - return; - } - - // All other wallet methods — execute silently (no task banner). - // These are read-only or background calls (executeUtility, simulateTx, batch - // without sendTx, getAccounts, etc.) that don't need user visibility. - (async () => { - let result = await sendToOffscreen({ - type: MessageTypes.WALLET_METHOD, - method: message.type, - args: message.args, - }); - - // MetaMask-like behavior: return only the active account for getAccounts - if (message.type === 'getAccounts' || message.type === 'getRegisteredAccounts') { - const activeData = await chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT); - const activeAddress = activeData[STORAGE_KEYS.ACTIVE_ACCOUNT]; - if (activeAddress && Array.isArray(result)) { - const activeAccount = result.find((acc: any) => - acc.item === activeAddress || acc.item?.toString() === activeAddress - ); - result = activeAccount ? [activeAccount] : result; - } - } - - await handler.sendResponse(session.sessionId, { - messageId: message.messageId, - result, - walletId: WALLET_CONFIG.walletId, - }); - })().catch(async (error: any) => { - log.error('[background] Error handling wallet message:', message.type, error); - await handler.sendResponse(session.sessionId, { - messageId: message.messageId, - error: error.message, - walletId: WALLET_CONFIG.walletId, - }); - }); -} - -// docs:start:callbacks -const callbacks: BackgroundConnectionCallbacks = { - onPendingDiscovery: async (discovery) => { - log.debug('[background] Pending discovery:', discovery.requestId, 'from', discovery.origin); - - // Clean up stale sessions from this tab (e.g. page refresh creates a new - // discovery while the old session is still in activeSessions). - for (const session of handler.getActiveSessions()) { - if (session.tabId === discovery.tabId) { - log.debug('[background] Terminating stale session for tab:', discovery.tabId, session.sessionId); - capabilitiesApprovedSessions.delete(session.sessionId); - queuedMessages.delete(session.sessionId); - handler.terminateSession(session.sessionId); - } - } - - // Deduplicate: reject any existing discovery from the same tab - const existing = handler.getPendingDiscoveries().find( - (d) => d.tabId === discovery.tabId && d.requestId !== discovery.requestId - ); - if (existing) { - handler.rejectDiscovery(existing.requestId); - } - - // Auto-approve if origin is already trusted (reconnection after page refresh) - if (await isTrustedOrigin(discovery.origin, discovery.appId)) { - log.debug('[background] Auto-approving trusted origin:', discovery.origin); - handler.approveDiscovery(discovery.requestId); - return; - } - - updateBadge(); - - openPopupWithFallback(); - }, - - onSessionEstablished: async (session: ActiveSession) => { - log.debug('[background] Session established:', session.sessionId); - - // Auto-confirm if origin is already trusted (skip emoji verification) - if (await isTrustedOrigin(session.origin, session.appId)) { - log.debug('[background] Auto-confirming trusted session:', session.sessionId); - - // Pre-approve capabilities if previously granted (enables seamless reconnect) - const savedCaps = await getStoredCapabilities(session.origin, session.appId); - if (savedCaps) { - capabilitiesApprovedSessions.add(session.sessionId); - } - - // Flush any queued messages immediately (same logic as CONFIRM_SESSION handler) - const queued = queuedMessages.get(session.sessionId) ?? []; - queuedMessages.delete(session.sessionId); - for (const { session: s, message: msg } of queued) { - processWalletMessage(s, msg); - } - pushStateToPopup(); - return; - } - - // New origin — require emoji verification - log.debug('[background] Awaiting emoji verification for:', session.sessionId); - // SDK automatically removes the discovery when key exchange completes. - // Show emojis in approvals so user can compare with the webapp - pendingSessionVerifications.push({ - sessionId: session.sessionId, - origin: session.origin, - appId: session.appId, - verificationHash: session.verificationHash, - timestamp: Date.now(), - }); - updateBadge(); - - // Only open popup if not already connected — calling openPopup() on an - // already-open popup rejects, and the fallback creates a second window - // that steals the popupPort from the original. - if (!popupPort) { - openPopupWithFallback(); - } - - pushStateToPopup(); - }, - - // docs:start:on-wallet-message - /** - * Handles wallet method calls from the ExtensionWallet proxy. - * Messages are queued while emoji verification is pending — the extension - * user must confirm before any dApp calls are processed. - */ - onWalletMessage: async (session: ActiveSession, message: any) => { - log.debug('[background] Wallet message:', message.type, 'from session:', session.sessionId); - - // Block wallet messages until the user confirms emoji verification in the extension. - // The dApp's calls (e.g. getAccounts) will wait until the extension user approves. - const awaitingVerification = pendingSessionVerifications.some( - (v) => v.sessionId === session.sessionId - ); - if (awaitingVerification) { - log.debug('[background] Session awaiting verification, queuing message:', message.type); - const queue = queuedMessages.get(session.sessionId) ?? []; - queue.push({ session, message }); - queuedMessages.set(session.sessionId, queue); - return; - } - - await processWalletMessage(session, message); - }, - // docs:end:on-wallet-message -}; -// docs:end:callbacks - -const handler = new BackgroundConnectionHandler(WALLET_CONFIG, transport, callbacks); -handler.initialize(); - -// Clean up sessions when a tab is closed -chrome.tabs.onRemoved.addListener((tabId) => { - const sessions = handler.getActiveSessions().filter((s) => s.tabId === tabId); - for (const session of sessions) { - capabilitiesApprovedSessions.delete(session.sessionId); - queuedMessages.delete(session.sessionId); - } - handler.terminateForTab(tabId); - pushStateToPopup(); -}); - -// docs:start:popup-messages -/** - * Handle messages from popup and offscreen for approvals and account management. - */ -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - /** - * Storage proxy for the offscreen document. (#7) - * Validates that the request comes from the extension itself (not content scripts or external). - */ - if (message.type === 'storage-get' || message.type === 'storage-set') { - // Security: only allow storage proxy from extension pages (offscreen, popup) (#7) - // Content scripts have sender.tab set; extension pages (offscreen, popup) do not. - if (sender.tab) { - log.warn('[background] Rejected storage proxy from content script, tab:', sender.tab.id); - sendResponse({ success: false, error: 'Storage proxy not allowed from content scripts' }); - return false; - } - - if (message.type === 'storage-get') { - chrome.storage.local.get(message.key).then((result) => { - sendResponse({ success: true, result: result[message.key] }); - }).catch((err) => { - sendResponse({ success: false, error: err.message }); - }); - } else { - chrome.storage.local.set(message.data).then(() => { - sendResponse({ success: true }); - }).catch((err) => { - sendResponse({ success: false, error: err.message }); - }); - } - return true; // async response (#23) - } - - if (message.target !== MessageTarget.BACKGROUND) { - return false; - } - - log.debug('[background] Popup message:', message.type); - - // Reset auto-lock on any popup interaction (#28) - resetAutoLockTimer(); - - switch (message.type) { - case MessageTypes.APPROVE_CONNECTION: { - handler.approveDiscovery(message.requestId); - updateBadge(); - sendResponse({ success: true }); - return false; // sync response (#23) - } - - case MessageTypes.REJECT_CONNECTION: { - handler.rejectDiscovery(message.requestId); - updateBadge(); - sendResponse({ success: true }); - return false; - } - - case MessageTypes.APPROVE_TRANSACTION: { - const pending = pendingTransactions.find( - (t) => t.messageId === message.messageId - ); - if (pending) { - pendingTransactions = pendingTransactions.filter( - (t) => t.messageId !== message.messageId - ); - updateBadge(); - - const taskId = startBackgroundTask(`tx:${pending.method}`, - handleTransactionApproval(pending) - ); - sendResponse({ success: true, result: { taskId } }); - } else { - sendResponse({ success: false, error: 'Transaction not found' }); - } - return false; - } - - case MessageTypes.REJECT_TRANSACTION: { - const pending = pendingTransactions.find( - (t) => t.messageId === message.messageId - ); - if (pending) { - handler.sendResponse(pending.sessionId, { - messageId: pending.messageId, - error: 'Transaction rejected by user', - walletId: WALLET_CONFIG.walletId, - }); - - pendingTransactions = pendingTransactions.filter( - (t) => t.messageId !== message.messageId - ); - updateBadge(); - } - sendResponse({ success: true }); - return false; - } - - case MessageTypes.APPROVE_CAPABILITIES: { - const pending = pendingCapabilities.find( - (c) => c.messageId === message.messageId - ); - if (pending) { - pendingCapabilities = pendingCapabilities.filter( - (c) => c.messageId !== message.messageId - ); - - // Build granted capabilities using the shared active-account helper - getGrantableAccounts().then((grantedAccounts) => { - const granted = pending.capabilities.map((cap: any) => { - if (cap.type === 'accounts') { - return { ...cap, accounts: grantedAccounts }; - } - return { ...cap }; - }); - - return handler.sendResponse(pending.sessionId, { - messageId: pending.messageId, - result: { - version: '1.0', - granted, - wallet: { - name: WALLET_CONFIG.walletName, - version: WALLET_CONFIG.walletVersion, - }, - }, - walletId: WALLET_CONFIG.walletId, - }); - }).then(async () => { - capabilitiesApprovedSessions.add(pending.sessionId); - - // Persist granted capabilities for auto-reconnect - const approvedSession = handler.getSession(pending.sessionId); - if (approvedSession) { - const trusted = await getTrustedOrigins(); - const entry = trusted.find(t => t.origin === approvedSession.origin && t.appId === approvedSession.appId); - if (entry) { - entry.grantedCapabilities = pending.capabilities.map((cap: any) => ({ ...cap })); - await chrome.storage.local.set({ [TRUSTED_ORIGINS_KEY]: trusted }); - } - } - - updateBadge(); - sendResponse({ success: true }); - }).catch((err) => { - log.error('[background] Failed to approve capabilities:', err); - sendResponse({ success: false, error: getErrorMessage(err) }); - }); - } else { - sendResponse({ success: false, error: 'Capability request not found' }); - return false; - } - return true; - } - - case MessageTypes.REJECT_CAPABILITIES: { - const pending = pendingCapabilities.find( - (c) => c.messageId === message.messageId - ); - if (pending) { - pendingCapabilities = pendingCapabilities.filter( - (c) => c.messageId !== message.messageId - ); - - handler.sendResponse(pending.sessionId, { - messageId: pending.messageId, - result: { - version: '1.0', - granted: [], - wallet: { - name: WALLET_CONFIG.walletName, - version: WALLET_CONFIG.walletVersion, - }, - }, - walletId: WALLET_CONFIG.walletId, - }); - - updateBadge(); - } - sendResponse({ success: true }); - return false; - } - - case MessageTypes.CONFIRM_SESSION: { - // User confirmed emojis match — session is now fully active. - // Flush any wallet messages that were queued while awaiting verification. - pendingSessionVerifications = pendingSessionVerifications.filter( - (v) => v.sessionId !== message.sessionId - ); - const queued = queuedMessages.get(message.sessionId) ?? []; - queuedMessages.delete(message.sessionId); - for (const { session, message: msg } of queued) { - log.debug('[background] Flushing queued message:', msg.type); - processWalletMessage(session, msg); - } - - // Remember this origin as trusted for future reconnections (#30) - const confirmedSession = handler.getSession(message.sessionId); - if (confirmedSession) { - addTrustedOrigin(confirmedSession.origin, confirmedSession.appId); - } - - updateBadge(); - sendResponse({ success: true }); - return false; - } - - case MessageTypes.REJECT_SESSION: { - // User rejected emoji verification — reject queued messages and terminate the session. - pendingSessionVerifications = pendingSessionVerifications.filter( - (v) => v.sessionId !== message.sessionId - ); - const rejected = queuedMessages.get(message.sessionId) ?? []; - queuedMessages.delete(message.sessionId); - for (const { session, message: msg } of rejected) { - handler.sendResponse(session.sessionId, { - messageId: msg.messageId, - error: 'Session verification rejected by user', - walletId: WALLET_CONFIG.walletId, - }); - } - handler.terminateSession(message.sessionId); - updateBadge(); - sendResponse({ success: true }); - return false; - } - - case MessageTypes.DISCONNECT_SESSION: { - // (#29) Allow users to disconnect a specific dApp session - // Remove from trusted origins so next connection requires full approval (#30) - const disconnectedSession = handler.getSession(message.sessionId); - if (disconnectedSession) { - removeTrustedOrigin(disconnectedSession.origin, disconnectedSession.appId); - } - capabilitiesApprovedSessions.delete(message.sessionId); - handler.terminateSession(message.sessionId); - pushStateToPopup(); - sendResponse({ success: true }); - return false; - } - - case 'getPendingItems': { - sendResponse({ - success: true, - result: getFullState(), - }); - return false; - } - - case MessageTypes.GET_ACCOUNTS: { - chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS) - .then((data) => { - const accounts = (data[STORAGE_KEYS.ACCOUNTS] || []).map((acc: any) => ({ - address: acc.address, - alias: acc.alias, - isDeployed: acc.isDeployed, - })); - sendResponse({ success: true, result: accounts }); - }) - .catch((error) => sendResponse({ success: false, error: error.message })); - return true; // async (#23) - } - - case MessageTypes.GET_ACTIVE_ACCOUNT: { - chrome.storage.local.get(STORAGE_KEYS.ACTIVE_ACCOUNT) - .then((data) => { - sendResponse({ success: true, result: data[STORAGE_KEYS.ACTIVE_ACCOUNT] || null }); - }) - .catch((error) => sendResponse({ success: false, error: error.message })); - return true; - } - - case MessageTypes.SET_ACTIVE_ACCOUNT: { - chrome.storage.local.set({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: message.address }) - .then(() => sendResponse({ success: true })) - .catch((error) => sendResponse({ success: false, error: error.message })); - return true; - } - - case MessageTypes.UNLOCK_WALLET: { - const taskId = startBackgroundTask('unlock', - sendToOffscreen({ - type: MessageTypes.UNLOCK_WALLET, - password: message.password, - }).then((result) => { - walletUnlocked = true; - persistState(); - resetAutoLockTimer(); - return result; - }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - case MessageTypes.GET_WALLET_STATUS: { - chrome.storage.local.get(STORAGE_KEYS.PASSWORD_DATA) - .then((data) => { - sendResponse({ - success: true, - result: { - unlocked: walletUnlocked, - hasPassword: !!data[STORAGE_KEYS.PASSWORD_DATA], - }, - }); - }) - .catch((error) => sendResponse({ success: false, error: error.message })); - return true; - } - - case MessageTypes.SETUP_PASSWORD: { - const taskId = startBackgroundTask('setup-password', - sendToOffscreen({ - type: MessageTypes.SETUP_PASSWORD, - password: message.password, - }).then((result) => { - walletUnlocked = true; - persistState(); - resetAutoLockTimer(); - return result; - }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - case MessageTypes.MARK_DEPLOYED: { - chrome.storage.local.get(STORAGE_KEYS.ACCOUNTS) - .then((data) => { - const accounts = data[STORAGE_KEYS.ACCOUNTS] || []; - const account = accounts.find((a: any) => a.address === message.address); - if (account) { - account.isDeployed = true; - return chrome.storage.local.set({ [STORAGE_KEYS.ACCOUNTS]: accounts }); - } - }) - .then(() => sendResponse({ success: true, result: { success: true } })) - .catch((error) => sendResponse({ success: false, error: error.message })); - return true; - } - - case MessageTypes.CREATE_ACCOUNT: { - const taskId = startBackgroundTask('create-account', - sendToOffscreen({ - type: MessageTypes.CREATE_ACCOUNT, - alias: message.alias, - }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - case MessageTypes.DEPLOY_ACCOUNT: { - const taskId = startBackgroundTask('deploy-account', - sendToOffscreen({ - type: MessageTypes.DEPLOY_ACCOUNT, - address: message.address, - }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - case MessageTypes.EXPORT_WALLET: { - const taskId = startBackgroundTask('export-wallet', - sendToOffscreen({ type: MessageTypes.EXPORT_WALLET }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - case MessageTypes.IMPORT_WALLET: { - // Wipe wallet data from chrome.storage.local and lock the wallet - chrome.storage.local.remove([STORAGE_KEYS.ACCOUNTS, STORAGE_KEYS.PASSWORD_DATA, STORAGE_KEYS.ACTIVE_ACCOUNT]); - walletUnlocked = false; - persistState(); - // Tell offscreen to clear cached key - sendToOffscreen({ type: MessageTypes.LOCK_WALLET }).catch(() => {}); - sendResponse({ success: true, result: { success: true } }); - return false; - } - - case MessageTypes.IMPORT_WALLET_ACCOUNTS: { - const taskId = startBackgroundTask('import-wallet-accounts', - sendToOffscreen({ - type: MessageTypes.IMPORT_WALLET_ACCOUNTS, - accounts: message.accounts, - activeAccount: message.activeAccount, - }).then((result) => { - walletUnlocked = true; - persistState(); - resetAutoLockTimer(); - return result; - }) - ); - sendResponse({ success: true, result: { taskId } }); - return false; - } - - default: { - log.warn('[background] Unknown message type:', message.type); - return false; - } - } -}); - -async function handleTransactionApproval( - pending: PendingTransaction -): Promise { - try { - const result = await sendToOffscreen({ - type: MessageTypes.WALLET_METHOD, - method: pending.method, - args: pending.args, - }); - - await handler.sendResponse(pending.sessionId, { - messageId: pending.messageId, - result, - walletId: WALLET_CONFIG.walletId, - }); - - return result; - } catch (error: any) { - log.error('[background] Transaction approval failed:', pending.method, error); - await handler.sendResponse(pending.sessionId, { - messageId: pending.messageId, - error: error.message, - walletId: WALLET_CONFIG.walletId, - }); - throw error; - } -} -// docs:end:popup-messages - -/** - * Extension lifecycle handlers. (#17) - */ -chrome.runtime.onInstalled.addListener((details) => { - log.debug('[background] Extension installed/updated:', details.reason); - - // Clear stale pending state — sessions don't survive extension reload - pendingTransactions = []; - pendingSessionVerifications = []; - pendingCapabilities = []; - queuedMessages.clear(); - capabilitiesApprovedSessions.clear(); - persistState(); - - if (details.reason === 'install') { - // First install — nothing to migrate - log.debug('[background] First install, no migration needed'); - } else if (details.reason === 'update') { - // Version update — could add migration logic here - log.debug('[background] Updated from', details.previousVersion); - } -}); - -// Restore state on service worker startup (#8) -restoreState().then(() => { - log.debug('[background] Service worker initialized'); -}); - -// Eagerly preload the offscreen document so WASM and PXE deps are warm -ensureOffscreenDocument().then(() => { - log.debug('[background] Offscreen document preloaded'); -}).catch((err) => { - log.error('[background] Offscreen preload failed (will retry on demand):', err); -}); diff --git a/docs/examples/webapp-tutorial/test-extension/src/config.ts b/docs/examples/webapp-tutorial/test-extension/src/config.ts deleted file mode 100644 index 00c7e3e00eee..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/config.ts +++ /dev/null @@ -1,96 +0,0 @@ -// docs:start:wallet-config -/** - * Configuration for the Aztec Tutorial Wallet extension. - * Uses SponsoredFPC for fee payment. - */ - -/** Aztec node URL — defaults to a local sandbox. */ -export const NODE_URL = 'http://localhost:8080'; - -/** Current @aztec/* package version, injected at build time by Vite. */ -declare const __AZTEC_PACKAGES_VERSION__: string; -export const AZTEC_PACKAGES_VERSION: string = - typeof __AZTEC_PACKAGES_VERSION__ !== 'undefined' ? __AZTEC_PACKAGES_VERSION__ : 'unknown'; - -/** Wallet identification for the SDK protocol */ -export const WALLET_CONFIG = { - walletId: 'aztec-tutorial-wallet', - walletName: 'Aztec Tutorial Wallet', - walletVersion: '1.0.0', - walletIcon: 'data:image/svg+xml,🔮', -}; - -/** Auto-lock timeout in minutes. The wallet locks after this period of inactivity. (#28) */ -export const AUTO_LOCK_MINUTES = 15; - -/** Message types for internal extension communication */ -export const MessageTypes = { - // Account management - GET_ACCOUNTS: 'get-accounts', - MARK_DEPLOYED: 'mark-deployed', - - // Full account creation in extension (uses Barretenberg) - CREATE_ACCOUNT: 'create-account', - DEPLOY_ACCOUNT: 'deploy-account', - - // Master password + wallet unlock - SETUP_PASSWORD: 'setup-password', - UNLOCK_WALLET: 'unlock-wallet', - GET_WALLET_STATUS: 'get-wallet-status', - - // PXE operations - INIT_PXE: 'init-pxe', - REGISTER_ACCOUNT: 'register-account', - - // Active account management - GET_ACTIVE_ACCOUNT: 'get-active-account', - SET_ACTIVE_ACCOUNT: 'set-active-account', - - // Wallet export/import - EXPORT_WALLET: 'export-wallet', - IMPORT_WALLET: 'import-wallet', - IMPORT_WALLET_ACCOUNTS: 'import-wallet-accounts', - - // Wallet SDK protocol — dispatches to BaseWallet - WALLET_METHOD: 'wallet-method', - - // Auto-lock - LOCK_WALLET: 'lock-wallet', - - // Popup -> Background - APPROVE_CONNECTION: 'approve-connection', - REJECT_CONNECTION: 'reject-connection', - APPROVE_TRANSACTION: 'approve-transaction', - REJECT_TRANSACTION: 'reject-transaction', - CONFIRM_SESSION: 'confirm-session', - REJECT_SESSION: 'reject-session', - DISCONNECT_SESSION: 'disconnect-session', - APPROVE_CAPABILITIES: 'approve-capabilities', - REJECT_CAPABILITIES: 'reject-capabilities', -} as const; - -/** Union type of all message type values — use for exhaustive checking. */ -export type MessageType = (typeof MessageTypes)[keyof typeof MessageTypes]; - -/** Targets for chrome.runtime messages */ -export const MessageTarget = { - OFFSCREEN: 'offscreen', - POPUP: 'popup', - BACKGROUND: 'background', -} as const; - - -/** - * Conditional logging. (#26) - * Strips verbose logs in production while keeping errors visible. - * Set DEBUG=true in the build to enable verbose logging. - */ -const DEBUG = process.env.NODE_ENV !== 'production'; // Toggle via build environment - -export const log = { - debug: (...args: unknown[]) => { if (DEBUG) console.log(...args); }, - info: (...args: unknown[]) => { if (DEBUG) console.info(...args); }, - warn: (...args: unknown[]) => console.warn(...args), - error: (...args: unknown[]) => console.error(...args), -}; -// docs:end:wallet-config diff --git a/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts b/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts deleted file mode 100644 index 51e8863edb22..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/offscreen/offscreen.ts +++ /dev/null @@ -1,771 +0,0 @@ -/** Offscreen document for the Aztec Tutorial Wallet. */ - -// CRITICAL: console-intercept MUST be the very first import. -// Pino (used by PXE) captures `console.info` at logger-creation time. -// By overriding it in a separate module imported first, ES module execution -// order guarantees the override is in place before any pino logger is created. -import { onConsoleInfo } from './console-intercept'; - -import { NODE_URL, MessageTypes, AZTEC_PACKAGES_VERSION, log } from '../config'; -import type { WalletExportData } from '../shared-types'; -import { - createAccount, - getAccounts, - getAccountSecret, - markDeployed, - storeAccount, - getActiveAccount, - setActiveAccount, -} from '../wallet/wallet-impl'; -import type { PXE } from '@aztec/pxe/client/lazy'; -import type { Account } from '@aztec/aztec.js/account'; -import type { AztecNode } from '@aztec/aztec.js/node'; -import { createAztecNodeClient } from '@aztec/aztec.js/node'; -import { getPXEConfig } from '@aztec/pxe/config'; -import { createPXE } from '@aztec/pxe/client/lazy'; - -// ============================================================================ -// CRITICAL: Enable SharedArrayBuffer support for Barretenberg WASM -// ============================================================================ -// -// Chrome extensions have SharedArrayBuffer available but crossOriginIsolated=false. -// bb.js checks crossOriginIsolated to decide which WASM binary to load and -// whether to create shared WebAssembly.Memory. -// -// This patches the main thread. Worker files are patched at build time by the -// patchWorkersCrossOriginIsolated Vite plugin (see vite.extension.config.ts). -// ============================================================================ -if (typeof SharedArrayBuffer !== 'undefined' && !(globalThis as any).crossOriginIsolated) { - Object.defineProperty(globalThis, 'crossOriginIsolated', { - value: true, - writable: false, - configurable: true, - }); -} - -import { getChromeRuntime, getErrorMessage } from '../utils'; -import { getAztecCore, getAztecWallet, getAztecDeploy } from '../aztec-imports'; -import { instantiateAccount } from '../account-utils'; - -const chromeRuntime = getChromeRuntime(); -log.debug('[offscreen] Offscreen document loaded. Storage proxied through background. Barretenberg enabled via crossOriginIsolated patch.'); - -/** - * Port connection from the background script. - * Set when the background connects via chrome.runtime.connect({ name: 'offscreen' }). - */ -let backgroundPort: chrome.runtime.Port | null = null; - -/** - * Sends a progress update to the background script for display in the popup. - * Fire-and-forget — uses the persistent port instead of broadcast sendMessage. - */ -function reportProgress(stage: string) { - log.debug('[offscreen] Progress:', stage); - backgroundPort?.postMessage({ type: 'task-progress', stage }); -} - -/** - * PXE log matchers — match pino browser log messages and relay as progress updates. - * - * Pino browser mode with `asObject: false` calls: - * console.info(bindingsObj, dataObj, messageString) - * where bindingsObj contains `{ module: 'pxe:service' }` etc. - * - * The interception is set up in console-intercept.ts (imported first) so that - * pino captures our wrapped console.info, not the original. - */ -const PXE_STAGE_MATCHERS: Array<{ module: string; pattern: RegExp; stage: string }> = [ - { module: 'pxe:service', pattern: /^Simulating transaction/, stage: 'Simulating transaction...' }, - { module: 'pxe:service', pattern: /^Simulation completed/, stage: 'Simulation complete, proving...' }, - { module: 'pxe:private-kernel-execution-prover', pattern: /^Private kernel witness generation/, stage: 'Kernel witness generated, creating proof...' }, - { module: 'pxe:bb:wasm:bundle', pattern: /^Generating ClientIVC proof/, stage: 'Generating ZK proof (this takes a while)...' }, - { module: 'pxe:bb:wasm:bundle', pattern: /^Generated ClientIVC proof/, stage: 'Proof generated, sending...' }, - { module: 'wallet-sdk:base_wallet', pattern: /^Sent transaction/, stage: 'Transaction sent, awaiting confirmation...' }, -]; - -onConsoleInfo((args) => { - const bindings = args.find((a) => typeof a === 'object' && a !== null && typeof a.module === 'string'); - const message = [...args].reverse().find((a) => typeof a === 'string'); - - if (bindings && message) { - for (const matcher of PXE_STAGE_MATCHERS) { - if (bindings.module === matcher.module && matcher.pattern.test(message)) { - reportProgress(matcher.stage); - break; - } - } - } -}); - -/** - * Master CryptoKey — cached in memory after unlock. (#2) - * - * This is a non-extractable AES-GCM CryptoKey derived from the user's password - * via PBKDF2. The raw password string is NEVER stored; only this opaque key - * object is kept in memory. Even if an attacker gets a reference to this object, - * they cannot extract the underlying key material (WebCrypto enforces this). - */ -let cachedMasterKey: CryptoKey | null = null; - -function getCachedMasterKey(): CryptoKey { - if (!cachedMasterKey) { - throw new Error('Wallet is locked. Please unlock first.'); - } - return cachedMasterKey; -} - -/** Clear the cached key (used for auto-lock). */ -export function clearCachedKey(): void { - cachedMasterKey = null; -} - -// docs:start:pxe-instance -/** - * PXE + node — lazily initialized as a pair, with dedup on the inflight promise. - */ -let pxeState: { pxe: PXE; node: AztecNode } | null = null; -let pxeInitializing: Promise<{ pxe: PXE; node: AztecNode }> | null = null; - -async function ensurePXE(nodeUrl: string = NODE_URL): Promise<{ pxe: PXE; node: AztecNode }> { - if (pxeState) return pxeState; - if (pxeInitializing) return pxeInitializing; - - log.debug('[offscreen] Initializing PXE with node:', nodeUrl); - pxeInitializing = (async () => { - try { - const node = createAztecNodeClient(nodeUrl); - const config = getPXEConfig(); - config.l1Contracts = await node.getL1ContractAddresses(); - const isLocal = nodeUrl.includes('localhost') || nodeUrl.includes('127.0.0.1'); - config.proverEnabled = !isLocal; - - const pxe = await createPXE(node, config, {}); - log.debug('[offscreen] PXE initialized, connected to node at:', nodeUrl); - - pxeState = { pxe, node }; - return pxeState; - } finally { - pxeInitializing = null; // Always clear so a retry can re-attempt - } - })(); - - return pxeInitializing; -} -// docs:end:pxe-instance - -// docs:start:wallet-instance -/** Single wallet class used for all operations. (#18, #20) */ - -import type { BaseWallet } from '@aztec/wallet-sdk/base-wallet'; - -/** - * The wallet instance holds a BaseWallet subclass with an additional - * registerAccount method for tracking which accounts we can sign for. - * BaseWallet is dynamically imported at runtime; using `import type` gives - * us the type without a runtime dependency. (#20) - */ -type OffscreenWalletType = BaseWallet & { registerAccount(address: string, account: Account): void }; - -let walletInstance: OffscreenWalletType | null = null; - -/** - * Creates a SponsoredFPC contract instance from its artifact and well-known salt. - * Shared between OffscreenWallet.ensureSponsoredFPC() and handleDeployAccount(). - */ -async function getSponsoredFPCInstance() { - const { Fr, SponsoredFPCContract, SPONSORED_FPC_SALT, getContractInstanceFromInstantiationParams } = await getAztecDeploy(); - return getContractInstanceFromInstantiationParams( - SponsoredFPCContract.artifact, - { salt: new Fr(SPONSORED_FPC_SALT) }, - ); -} - -async function getWallet() { - if (walletInstance) return walletInstance; - - const { BaseWallet, AztecAddress, SignerlessAccount } = await getAztecWallet(); - const { pxe, node } = await ensurePXE(); - - // AccountFeePaymentMethodOptions.EXTERNAL = 0 — fee is paid by an external FPC - const EXTERNAL_FEE_PAYMENT = 0; - - class OffscreenWallet extends BaseWallet { - protected minFeePadding = 1.0; // 100% padding for fee estimation variance - private accounts: Map = new Map(); - private sponsoredFPCAddress: any | null = null; - - constructor(pxeInstance: PXE, aztecNode: AztecNode) { - super(pxeInstance, aztecNode); - } - - registerAccount(address: string, account: Account) { - this.accounts.set(address, account); - } - - protected async getAccountFromAddress(address: any): Promise { - if (address.equals(AztecAddress.ZERO)) { - return new SignerlessAccount(); - } - const key = address.toString(); - const account = this.accounts.get(key); - if (!account) { - throw new Error(`Account not found for address: ${key}`); - } - return account; - } - - async getAccounts() { - return Array.from(this.accounts.entries()).map(([, acc]) => ({ - alias: '', - item: acc.getAddress(), - })); - } - - /** Lazily registers the SponsoredFPC contract and caches its address. */ - private async ensureSponsoredFPC() { - if (this.sponsoredFPCAddress) return this.sponsoredFPCAddress; - const { SponsoredFPCContract } = await getAztecDeploy(); - const sponsoredFPCInstance = await getSponsoredFPCInstance(); - await this.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); - this.sponsoredFPCAddress = sponsoredFPCInstance.address; - return this.sponsoredFPCAddress; - } - - // docs:start:complete-fee-options - /** - * Always uses SponsoredFPC for fee payment, mirroring the deployment flow. - * The tutorial wallet doesn't hold fee juice, so every tx is sponsor-paid. - * - * If the execution payload already has a feePayer (e.g. DeployAccountMethod - * embeds SponsoredFPC in its own payload), we skip injecting a wallet-level - * payment method to avoid calling sponsor_unconditionally() twice, which - * would trigger "Cannot enter the revertible phase twice". - */ - protected async completeFeeOptions(from: any, feePayer?: any, gasSettings?: any) { - const base = await super.completeFeeOptions(from, feePayer, gasSettings); - // If the payload already includes a fee payer, don't inject another one - if (feePayer) { - return { - ...base, - accountFeePaymentMethodOptions: EXTERNAL_FEE_PAYMENT, - }; - } - const address = await this.ensureSponsoredFPC(); - const { SponsoredFeePaymentMethod } = await getAztecDeploy(); - return { - ...base, - walletFeePaymentMethod: new SponsoredFeePaymentMethod(address), - accountFeePaymentMethodOptions: EXTERNAL_FEE_PAYMENT, - }; - } - // docs:end:complete-fee-options - - /** - * Overrides sendTx to auto-extract auth witnesses from offchain effects. - * - * dApps like gregoswap don't explicitly create auth witnesses. Instead, they - * expect the wallet to handle it: simulate with a stub account (which passes - * all auth checks), extract the authorization requests emitted by - * `#[authorize_once]` in Noir contracts, sign them, and include them in the - * real transaction. - */ - async sendTx(executionPayload: any, opts: any): Promise { - if (executionPayload.authWitnesses.length === 0 && opts.from && !opts.from.equals(AztecAddress.ZERO)) { - try { - await this.extractAndInjectAuthWitnesses(executionPayload, opts.from, opts.fee?.gasSettings); - } catch (err: any) { - log.error('[offscreen] Auth witness extraction failed, proceeding without:', err.message, err.stack); - } - } - return super.sendTx(executionPayload, opts); - } - - /** - * Simulates the tx with a stub account to collect offchain effects, - * parses CallAuthorizationRequest objects, and creates real auth witnesses. - */ - private async extractAndInjectAuthWitnesses(executionPayload: any, from: any, feeGasSettings?: any) { - const { Fr, getContractInstanceFromInstantiationParams } = await getAztecCore(); - - // Step 1: Create a stub account that passes all auth checks unconditionally - log.info('[offscreen] Step 1: Loading stub account module...'); - const realAccount = await this.getAccountFromAddress(from); - const originalAddress = realAccount.getCompleteAddress(); - log.info('[offscreen] Got complete address:', originalAddress.address.toString()); - - const { createStubAccount, getStubAccountContractArtifact } = await import('@aztec/accounts/stub/lazy'); - log.info('[offscreen] Loaded @aztec/accounts/stub/lazy'); - - const stubArtifact = await getStubAccountContractArtifact(); - log.info('[offscreen] Loaded stub artifact:', stubArtifact.name); - - const stubAccount = createStubAccount(originalAddress); - const stubInstance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random() }); - log.info('[offscreen] Created stub account and instance'); - - // Step 2: Simulate with the stub account swapped in via PXE overrides - log.info('[offscreen] Step 2: Simulating tx with stub account...'); - const feeOptions = await this.completeFeeOptions(from, executionPayload.feePayer, feeGasSettings); - const chainInfo = await this.getChainInfo(); - const txRequest = await stubAccount.createTxExecutionRequest( - executionPayload, - feeOptions.gasSettings, - chainInfo, - { txNonce: Fr.random(), cancellable: false, feePaymentMethodOptions: feeOptions.accountFeePaymentMethodOptions }, - ); - log.info('[offscreen] Created tx execution request, simulating...'); - - const simResult = await this.pxe.simulateTx(txRequest, { - simulatePublic: true, - skipTxValidation: true, - skipFeeEnforcement: true, - overrides: { contracts: { [from.toString()]: { instance: stubInstance, artifact: stubArtifact } } }, - scopes: [from], - }); - log.info('[offscreen] Simulation succeeded'); - - // Step 3: Extract auth witness requests from offchain effects - log.info('[offscreen] Step 3: Extracting offchain effects...'); - const { collectOffchainEffects } = await import('@aztec/stdlib/tx'); - const { CallAuthorizationRequest } = await import('@aztec/aztec.js/authorization'); - - if (!simResult.privateExecutionResult) { - log.warn('[offscreen] No privateExecutionResult in simulation result'); - return; - } - - const effects = collectOffchainEffects(simResult.privateExecutionResult); - log.info(`[offscreen] Found ${effects.length} offchain effect(s)`); - - // Pre-filter by CallAuthorizationRequest selector (matching e2e test pattern) - const callAuthSelector = await CallAuthorizationRequest.getSelector(); - const authEffects = effects.filter((e: any) => - e.data.length > 0 && e.data[0].equals(callAuthSelector.toField()), - ); - log.info(`[offscreen] ${authEffects.length} are CallAuthorizationRequest(s)`); - - // Step 4: Create auth witnesses from parsed authorization requests - let count = 0; - for (const effect of authEffects) { - const authRequest = await CallAuthorizationRequest.fromFields(effect.data); - log.info(`[offscreen] Auth request: consumer=${effect.contractAddress.toString()}, innerHash=${authRequest.innerHash.toString()}`); - const wit = await this.createAuthWit(from, { - consumer: effect.contractAddress, - innerHash: authRequest.innerHash, - }); - executionPayload.authWitnesses.push(wit); - count++; - log.info(`[offscreen] Created auth witness #${count}: messageHash=${wit.requestHash.toString()}`); - } - - log.info(`[offscreen] Auth witness extraction complete: ${count} witness(es) from ${effects.length} effect(s)`); - } - } - - walletInstance = new OffscreenWallet(pxe, node); - return walletInstance; -} -// docs:end:wallet-instance - -// docs:start:message-handler -/** - * Handles messages from the background script via a persistent port. - * The background connects with chrome.runtime.connect({ name: 'offscreen' }). - * Each message includes a messageId for request/response correlation. - */ -chromeRuntime.runtime.onConnect.addListener((port: chrome.runtime.Port) => { - if (port.name !== 'offscreen') return; - - log.debug('[offscreen] Background port connected'); - backgroundPort = port; - - port.onMessage.addListener((message: any) => { - log.debug('[offscreen] Received message:', message.type); - - handleMessage(message) - .then((result) => { - log.debug('[offscreen] Sending response for:', message.type); - port.postMessage({ messageId: message.messageId, success: true, result }); - }) - .catch((error: unknown) => { - const msg = getErrorMessage(error); - log.error('[offscreen] Error:', msg, error); - port.postMessage({ messageId: message.messageId, success: false, error: msg }); - }); - }); - - port.onDisconnect.addListener(() => { - log.debug('[offscreen] Background port disconnected'); - backgroundPort = null; - }); -}); - -async function handleMessage(message: any): Promise { - switch (message.type) { - case MessageTypes.GET_ACCOUNTS: - return handleGetAccounts(); - - case MessageTypes.MARK_DEPLOYED: - return handleMarkDeployed(message.address); - - case MessageTypes.WALLET_METHOD: - return handleWalletMethod(message.method, message.args); - - case MessageTypes.SETUP_PASSWORD: - return handleSetupPassword(message.password); - - case MessageTypes.CREATE_ACCOUNT: - return handleCreateAccount(message.alias); - - case MessageTypes.DEPLOY_ACCOUNT: - return handleDeployAccount(message.address); - - case MessageTypes.UNLOCK_WALLET: - return handleUnlockWallet(message.password); - - case MessageTypes.INIT_PXE: - return handleInitPXE(message.nodeUrl); - - case MessageTypes.REGISTER_ACCOUNT: - return handleRegisterAccount(message.address, message.secret, message.salt); - - case MessageTypes.EXPORT_WALLET: - return handleExportWallet(); - - case MessageTypes.IMPORT_WALLET_ACCOUNTS: - return handleImportWalletAccounts(message.accounts, message.activeAccount); - - // Lock the wallet (clear cached key) — used by auto-lock (#28) - case MessageTypes.LOCK_WALLET: - cachedMasterKey = null; - walletInstance = null; - return { success: true }; - - default: - throw new Error(`Unknown message type: ${message.type}`); - } -} -// docs:end:message-handler - -async function handleGetAccounts() { - return getAccounts(); -} - -async function handleMarkDeployed(address: string) { - await markDeployed(address); - return { success: true }; -} - -// docs:start:wallet-method-handler -/** - * Handles wallet method calls from the ExtensionWallet proxy via the SDK protocol. - * - * Serialization notes: - * 1. ARGS: Arrive as plain JSON. We use WalletSchema to parse them back into - * proper Aztec types (AztecAddress, Fr, ExecutionPayload, etc.). - * 2. RESULT: Contains class instances that lose prototypes through Chrome messaging. - * We serialize with jsonStringify before returning. - */ -async function handleWalletMethod(method: string, args: any[]): Promise { - log.debug('[offscreen] Handling wallet method:', method); - - const wallet = await getWallet(); - - // Dynamic dispatch: the wallet protocol sends method names as strings. - // Cast to Record for dynamic access since TypeScript can't know the method at compile time. - const walletObj = wallet as unknown as Record any>; - if (typeof walletObj[method] !== 'function') { - throw new Error(`Unknown wallet method: ${method}`); - } - - const { WalletSchema, jsonStringify, schemaHasMethod } = await getAztecWallet(); - - // Parse args through WalletSchema to reconstruct proper Aztec types (Buffer, Fr, etc.) - // from their JSON representations. The schema's .parameters() returns a zod tuple that - // requires all positional elements even if some are optional. Pad with undefined so the - // tuple length matches and the parse succeeds. - let parsedArgs: any[] = args || []; - if (schemaHasMethod(WalletSchema, method)) { - const schema = WalletSchema[method as keyof typeof WalletSchema]; - const paramSchema = schema.parameters(); - const expectedLength = (paramSchema as any)?._def?.items?.length ?? 0; - const paddedArgs = [...(args || [])]; - while (paddedArgs.length < expectedLength) { - paddedArgs.push(undefined); - } - try { - parsedArgs = await paramSchema.parseAsync(paddedArgs); - } catch (parseErr: any) { - log.warn('[offscreen] Args parse warning for', method, ':', parseErr.message); - parsedArgs = args || []; - } - } - - // Report initial progress for long-running methods so the popup shows something - // before the PXE log matchers kick in - const longRunningMethods = ['sendTx', 'simulateTx', 'profileTx']; - if (longRunningMethods.includes(method)) { - reportProgress(`Starting ${method}...`); - } - - const result = await walletObj[method](...parsedArgs); - - // Serialize to JSON-safe format before returning through Chrome messaging - const jsonSafe = JSON.parse(jsonStringify(result)); - log.debug('[offscreen] Wallet method completed:', method); - return jsonSafe; -} -// docs:end:wallet-method-handler - -/** - * Sets the master password for the first time. (#1, #2) - * Returns the CryptoKey (which we cache), never stores the password. - */ -async function handleSetupPassword(password: string) { - log.debug('[offscreen] Setting up master password'); - const { setupPassword, hasPassword: checkHasPassword } = await import('../wallet/storage'); - const exists = await checkHasPassword(); - if (exists) { - throw new Error('Master password already set'); - } - cachedMasterKey = await setupPassword(password); - // password string goes out of scope here — only the CryptoKey survives - return { success: true }; -} - -/** - * Creates an account using the cached master CryptoKey. (#2, #3) - */ -async function handleCreateAccount(alias: string) { - const masterKey = getCachedMasterKey(); - log.debug('[offscreen] Creating account with alias:', alias); - const result = await createAccount(masterKey, alias); - log.debug('[offscreen] Account created:', result.address); - return { address: result.address }; -} - -// docs:start:deploy-account -/** - * Deploys an account contract onchain using SponsoredFPC for fee payment. - * Uses the cached master CryptoKey to decrypt the account secret. (#2, #4) - */ -async function handleDeployAccount(address: string) { - const masterKey = getCachedMasterKey(); - log.debug('[offscreen] Deploying account:', address); - - // 1. Decrypt the account secret - reportProgress('Decrypting account secret...'); - const secretData = await getAccountSecret(address, masterKey); - if (!secretData) { - throw new Error(`Account not found: ${address}`); - } - - // 2. Ensure PXE is initialized (needed by the wallet) - reportProgress('Connecting to PXE...'); - await ensurePXE(); - - // 3. Register account with PXE and wallet (shared with unlock flow) - reportProgress('Registering account contract...'); - const { accountManager } = await registerAccountInWallet(address, secretData.secret, secretData.salt); - - // 4. Register SponsoredFPC contract with PXE (shared helper) - reportProgress('Registering fee payment contract...'); - const { AztecAddress, SponsoredFeePaymentMethod, SponsoredFPCContract } = await getAztecDeploy(); - const sponsoredFPCInstance = await getSponsoredFPCInstance(); - const wallet = await getWallet(); - await wallet.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact); - - // 5. Deploy with SponsoredFPC fee payment. - // PXE log matchers (PXE_STAGE_MATCHERS) provide granular progress updates - // (simulating → proving → proof generated → sending → awaiting confirmation). - reportProgress('Starting deploy tx...'); - - const paymentMethod = new SponsoredFeePaymentMethod(sponsoredFPCInstance.address); - const deployMethod = await accountManager.getDeployMethod(); - const receipt = await deployMethod.send({ - from: AztecAddress.ZERO, - fee: { paymentMethod }, - wait: { timeout: 2400 }, - }); - - // 6. Mark deployed in storage - await markDeployed(address); - reportProgress('Deploy complete!'); - - log.debug('[offscreen] Account deployed:', address, 'txHash:', receipt.txHash?.toString()); - return { success: true, txHash: receipt.txHash?.toString() }; -} -// docs:end:deploy-account - -// docs:start:load-accounts -/** - * Unlocks the wallet: verifies password, caches CryptoKey, initializes PXE, - * registers all stored accounts. (#1, #2) - * - * After unlock: - * - cachedMasterKey holds the non-extractable CryptoKey - * - The password string is discarded (goes out of scope) - * - All accounts are registered with PXE and the BaseWallet - */ -async function handleUnlockWallet(password: string) { - log.debug('[offscreen] Unlocking wallet...'); - - reportProgress('Verifying password...'); - const { verifyAndDeriveMasterKey, hasPassword: checkHasPassword } = await import('../wallet/storage'); - - if (await checkHasPassword()) { - const masterKey = await verifyAndDeriveMasterKey(password); - if (!masterKey) { - throw new Error('Incorrect password'); - } - cachedMasterKey = masterKey; - // password string goes out of scope — only the CryptoKey survives - } else { - throw new Error('No password set. Please set up your wallet first.'); - } - - // Initialize PXE - reportProgress('Initializing PXE (loading WASM)...'); - await ensurePXE(); - log.debug('[offscreen] PXE initialized for unlock'); - - // Register all stored accounts - const storedAccounts = await getAccounts(); - reportProgress(`Registering ${storedAccounts.length} account(s)...`); - log.debug('[offscreen] Registering', storedAccounts.length, 'accounts with PXE'); - - const failedAccounts: string[] = []; - for (const account of storedAccounts) { - try { - const secretData = await getAccountSecret(account.address, cachedMasterKey); - if (secretData) { - await registerAccountInWallet(account.address, secretData.secret, secretData.salt); - log.debug('[offscreen] Registered account:', account.address); - } - } catch (err: any) { - log.error('[offscreen] Failed to register account:', account.address, err.message); - failedAccounts.push(account.address); - // Continue with remaining accounts — partial unlock is better than full lockout - } - } - - if (failedAccounts.length === storedAccounts.length && storedAccounts.length > 0) { - // ALL accounts failed — password is likely wrong - cachedMasterKey = null; - throw new Error('Failed to unlock: wrong password or corrupted data'); - } - if (failedAccounts.length > 0) { - log.warn('[offscreen] Partial unlock:', failedAccounts.length, 'account(s) failed to register'); - } - - log.debug('[offscreen] Wallet unlocked,', storedAccounts.length - failedAccounts.length, 'of', storedAccounts.length, 'accounts registered'); - return { success: true }; -} -// docs:end:load-accounts - -/** - * Registers an account with both PXE and the BaseWallet. - * Returns the AccountManager so callers (e.g. deploy) can use it for further operations. - */ -async function registerAccountInWallet(address: string, secret: string, salt: string) { - const { secretFr, saltFr, accountContract, artifact, instance } = - await instantiateAccount(secret, salt); - - const { AccountManager } = await getAztecCore(); - - const wallet = await getWallet(); - await wallet.registerContract(instance, artifact, secretFr); - - const accountManager = await AccountManager.create(wallet, secretFr, accountContract, saltFr); - const account = await accountManager.getAccount(); - wallet.registerAccount(address, account); - - log.debug('[offscreen] Account registered in PXE and wallet:', address); - return { success: true, address, accountManager }; -} - -async function handleRegisterAccount(address: string, secret: string, salt: string) { - return registerAccountInWallet(address, secret, salt); -} - -/** - * Exports the entire wallet: decrypts all account secrets and builds a WalletExportData object. - */ -async function handleExportWallet(): Promise { - const masterKey = getCachedMasterKey(); - const allAccounts = await getAccounts(); - const activeAddr = await getActiveAccount(); - - reportProgress(`Decrypting ${allAccounts.length} account(s)...`); - - const exportedAccounts: WalletExportData['accounts'] = []; - for (const account of allAccounts) { - const secretData = await getAccountSecret(account.address, masterKey); - if (!secretData) { - throw new Error(`Failed to decrypt account: ${account.address}`); - } - exportedAccounts.push({ - address: account.address, - secret: secretData.secret, - salt: secretData.salt, - alias: account.alias, - isDeployed: account.isDeployed, - }); - } - - return { - version: 1, - aztecPackagesVersion: AZTEC_PACKAGES_VERSION, - exportedAt: new Date().toISOString(), - accounts: exportedAccounts, - activeAccount: activeAddr, - }; -} - -/** - * Imports accounts into the wallet: re-encrypts each account with the new master key, - * stores them, marks deployed ones, sets active account, and registers all with PXE. - */ -async function handleImportWalletAccounts( - accounts: WalletExportData['accounts'], - activeAccount: string | null, -): Promise<{ success: true }> { - const masterKey = getCachedMasterKey(); - - reportProgress(`Importing ${accounts.length} account(s)...`); - - for (const account of accounts) { - await storeAccount( - account.address, - account.secret, - account.salt, - masterKey, - account.alias, - ); - if (account.isDeployed) { - await markDeployed(account.address); - } - } - - if (activeAccount) { - await setActiveAccount(activeAccount); - } - - // Initialize PXE and register all accounts - reportProgress('Initializing PXE...'); - await ensurePXE(); - - for (const account of accounts) { - reportProgress(`Registering ${account.alias || account.address.slice(0, 10)}...`); - await registerAccountInWallet(account.address, account.secret, account.salt); - } - - reportProgress('Import complete!'); - return { success: true }; -} - -async function handleInitPXE(nodeUrl?: string) { - await ensurePXE(nodeUrl); - return { success: true }; -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx deleted file mode 100644 index ff08b6674353..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/AccountSwitcher.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -import type { StoredAccount } from './types'; -import { truncateAddress } from './helpers'; - -interface AccountSwitcherProps { - accounts: StoredAccount[]; - activeAccount: string | null; - onSelect: (address: string) => void; - onCreateNew: () => void; -} - -export function AccountSwitcher({ accounts, activeAccount, onSelect, onCreateNew }: AccountSwitcherProps) { - return ( -
- {accounts.map((account) => { - const isActive = activeAccount === account.address; - return ( -
onSelect(account.address)} - > -
-
- {account.alias || 'Unnamed Account'} -
-
- {truncateAddress(account.address)} -
-
-
- - {account.isDeployed ? 'Deployed' : 'Not Deployed'} - - {isActive && } -
-
- ); - })} - - -
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx deleted file mode 100644 index 51fdf367811a..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/ApprovalView.tsx +++ /dev/null @@ -1,509 +0,0 @@ -import React, { useState } from 'react'; - -import { MessageTypes } from '../config'; -import { getOriginHost } from '../utils'; -import { hashToEmoji } from '@aztec/wallet-sdk/crypto'; -import type { PendingDiscovery } from '@aztec/wallet-sdk/extension/handlers'; -import type { PendingTransaction, PendingCapabilities, PendingSessionVerification } from '../shared-types'; -import { sendToBackground, truncateAddress } from './helpers'; - -interface ApprovalsViewProps { - discoveries: PendingDiscovery[]; - transactions: PendingTransaction[]; - pendingCapabilities: PendingCapabilities[]; - onRefresh: () => void; -} - -export function ApprovalsView({ - discoveries, - transactions, - pendingCapabilities, - onRefresh, -}: ApprovalsViewProps) { - const [processing, setProcessing] = useState(null); - const [error, setError] = useState(null); - - /** Generic approval/rejection handler — eliminates per-type boilerplate. */ - const handleAction = async (type: string, payload: Record, key: string) => { - try { - setProcessing(key); - setError(null); - await sendToBackground({ type, ...payload }); - } catch (err: any) { - // "Transaction not found" means it was already processed — just refresh - if (type !== MessageTypes.APPROVE_TRANSACTION) { - setError(err.message); - } else { - console.debug('Approve failed (likely stale):', err.message); - } - } finally { - setProcessing(null); - onRefresh(); - } - }; - - const handleApproveConnection = (requestId: string) => - handleAction(MessageTypes.APPROVE_CONNECTION, { requestId }, requestId); - const handleRejectConnection = (requestId: string) => - handleAction(MessageTypes.REJECT_CONNECTION, { requestId }, requestId); - const handleApproveTx = (messageId: string) => - handleAction(MessageTypes.APPROVE_TRANSACTION, { messageId }, messageId); - const handleRejectTx = (messageId: string) => - handleAction(MessageTypes.REJECT_TRANSACTION, { messageId }, messageId); - const handleApproveCapabilities = (messageId: string) => - handleAction(MessageTypes.APPROVE_CAPABILITIES, { messageId }, messageId); - const handleRejectCapabilities = (messageId: string) => - handleAction(MessageTypes.REJECT_CAPABILITIES, { messageId }, messageId); - - if (discoveries.length === 0 && transactions.length === 0 && pendingCapabilities.length === 0) { - return ( -
-
-
No pending approvals
-
- ); - } - - return ( -
- {error &&
{error}
} - - {discoveries.map((discovery) => ( - handleApproveConnection(discovery.requestId)} - onReject={() => handleRejectConnection(discovery.requestId)} - processing={processing === discovery.requestId} - /> - ))} - - {transactions.map((tx) => ( - handleApproveTx(tx.messageId)} - onReject={() => handleRejectTx(tx.messageId)} - processing={processing === tx.messageId} - /> - ))} - - {pendingCapabilities.map((cap) => ( - handleApproveCapabilities(cap.messageId)} - onReject={() => handleRejectCapabilities(cap.messageId)} - processing={processing === cap.messageId} - /> - ))} -
- ); -} - -export function SessionVerificationView({ verification, onConfirm, onReject }: { - verification: PendingSessionVerification; - onConfirm: () => void; - onReject: () => void; -}) { - const emojis = hashToEmoji(verification.verificationHash); - - return ( -
-
-
-
🔐
-
-
{getOriginHost(verification.origin)}
-
Verify Connection
-
-
- -
-

- Confirm these emojis match what the dApp shows: -

-
{emojis}
-
- -
- - -
-
-
- ); -} - -// docs:start:connection-approval -interface ConnectionApprovalProps { - discovery: PendingDiscovery; - onApprove: () => void; - onReject: () => void; - processing: boolean; -} - -function ConnectionApproval({ - discovery, - onApprove, - onReject, - processing, -}: ConnectionApprovalProps) { - return ( -
-
-
🔗
-
-
{getOriginHost(discovery.origin)}
-
Connection Request
-
-
- -
-
- Origin - {discovery.origin} -
- {discovery.appId && ( -
- App ID - {discovery.appId} -
- )} -
- -
- - -
-
- ); -} -// docs:end:connection-approval - -// docs:start:transaction-approval -interface TransactionApprovalProps { - transaction: PendingTransaction; - onApprove: () => void; - onReject: () => void; - processing: boolean; -} - -function TransactionApproval({ - transaction, - onApprove, - onReject, - processing, -}: TransactionApprovalProps) { - const methodLabels: Record = { - sendTx: 'Send Transaction', - simulateTx: 'Simulate Transaction', - createAuthWit: 'Create Authorization', - profileTx: 'Profile Transaction', - batch: 'Batch Transaction', - }; - - return ( -
-
-
📝
-
-
{getOriginHost(transaction.origin)}
-
- {methodLabels[transaction.method] || transaction.method} -
-
-
- -
-
- From - {truncateAddress(transaction.from)} -
-
- Method - {transaction.method} -
- - {/* sendTx: show function calls from the execution payload (args[0]) */} - {transaction.method === 'sendTx' && transaction.args?.[0]?.calls && ( -
-
Function Calls:
- {transaction.args[0].calls.map((call: any, i: number) => ( -
-
{call.name || 'Unknown Function'}
-
Contract: {truncateAddress(call.to?.toString?.() || '')}
-
- ))} -
- )} - - {/* batch: show list of batched operations and their function calls */} - {transaction.method === 'batch' && Array.isArray(transaction.args?.[0]) && ( -
-
Batched Operations:
- {transaction.args[0].map((method: any, i: number) => ( -
-
- {methodLabels[method.name] || method.name} -
- {method.name === 'sendTx' && method.args?.[0]?.calls?.map((call: any, j: number) => ( -
- {call.name || 'Unknown'} → {truncateAddress(call.to?.toString?.() || '')} -
- ))} -
- ))} -
- )} -
- -
- - -
-
- ); -} -// docs:end:transaction-approval - -interface ExpandableSection { - summary: string; - items: string[]; -} - -interface CapabilityDescription { - label: string; - details: string[]; - expandable: ExpandableSection[]; -} - -function formatPatterns(scope: any): { summary: string; items: string[] } { - if (!Array.isArray(scope)) return { summary: 'unknown', items: [] }; - const items = scope.map((p: any) => { - const contract = p.contract === '*' ? '*' : truncateAddress(String(p.contract)); - const fn = p.function || '*'; - return `${contract}:${fn}`; - }); - return { summary: `${scope.length} specific pattern(s)`, items }; -} - -function describeCapability(cap: any): CapabilityDescription { - switch (cap.type) { - case 'accounts': { - const details = []; - if (cap.canGet) details.push('View account addresses'); - if (cap.canCreateAuthWit) details.push('Create authentication witnesses'); - return { label: 'Account Access', details: details.length ? details : ['Basic account access'], expandable: [] }; - } - case 'contracts': { - const expandable: ExpandableSection[] = []; - let scopeText: string; - if (cap.contracts === '*') { - scopeText = 'Scope: All contracts'; - } else if (Array.isArray(cap.contracts)) { - scopeText = `Scope: ${cap.contracts.length} specific contract(s)`; - expandable.push({ - summary: `${cap.contracts.length} specific contract(s)`, - items: cap.contracts.map((c: any) => truncateAddress(String(c))), - }); - } else { - scopeText = 'Scope: unknown'; - } - return { - label: 'Contract Access', - details: [ - cap.canRegister ? 'Register contracts' : '', - cap.canGetMetadata ? 'Query contract metadata' : '', - scopeText, - ].filter(Boolean), - expandable, - }; - } - case 'transaction': { - const expandable: ExpandableSection[] = []; - let scopeText: string; - if (cap.scope === '*') { - scopeText = 'Scope: Any transaction'; - } else if (Array.isArray(cap.scope)) { - const { summary, items } = formatPatterns(cap.scope); - scopeText = `Scope: ${summary}`; - expandable.push({ summary, items }); - } else { - scopeText = 'Scope: unknown'; - } - return { label: 'Send Transactions', details: [scopeText], expandable }; - } - case 'simulation': { - const expandable: ExpandableSection[] = []; - const details: string[] = []; - if (cap.transactions) { - if (cap.transactions.scope === '*') { - details.push('Tx simulation: any'); - } else if (Array.isArray(cap.transactions.scope)) { - const { summary, items } = formatPatterns(cap.transactions.scope); - details.push(`Tx simulation: ${summary}`); - expandable.push({ summary: `Tx: ${summary}`, items }); - } - } - if (cap.utilities) { - if (cap.utilities.scope === '*') { - details.push('Utility calls: any'); - } else if (Array.isArray(cap.utilities.scope)) { - const { summary, items } = formatPatterns(cap.utilities.scope); - details.push(`Utility calls: ${summary}`); - expandable.push({ summary: `Util: ${summary}`, items }); - } - } - return { label: 'Simulate Transactions', details, expandable }; - } - case 'data': - return { - label: 'Data Access', - details: [ - cap.addressBook ? 'Address book' : '', - cap.privateEvents ? 'Private events' : '', - ].filter(Boolean), - expandable: [], - }; - default: - return { label: cap.type, details: ['Unknown capability'], expandable: [] }; - } -} - -interface CapabilitiesApprovalProps { - pending: PendingCapabilities; - onApprove: () => void; - onReject: () => void; - processing: boolean; -} - -function CapabilitiesApproval({ - pending, - onApprove, - onReject, - processing, -}: CapabilitiesApprovalProps) { - const [expanded, setExpanded] = useState>(new Set()); - - const toggleExpanded = (key: string) => { - setExpanded((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - - return ( -
-
-
🔒
-
-
{getOriginHost(pending.origin)}
-
Capabilities Request
-
-
- -
-
- App - - {pending.appMetadata.name} v{pending.appMetadata.version} - -
- {pending.appMetadata.description && ( -
- Description - {pending.appMetadata.description} -
- )} -
- Origin - {pending.origin} -
- -
-
Requested Permissions:
- {pending.capabilities.map((cap, i) => { - const desc = describeCapability(cap); - return ( -
-
{desc.label}
- {desc.details.map((detail, j) => ( -
{detail}
- ))} - {desc.expandable.map((section, k) => { - const key = `${i}-${k}`; - const isExpanded = expanded.has(key); - return ( -
- - {isExpanded && ( -
- {section.items.map((item, l) => ( -
{item}
- ))} -
- )} -
- ); - })} -
- ); - })} -
-
- -
- - -
-
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx deleted file mode 100644 index 2201124a29f8..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/CreateAccountView.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useState } from 'react'; - -import { createAndActivateAccount } from './helpers'; - -export function CreateAccountView({ onCreated }: { onCreated: () => void }) { - const [alias, setAlias] = useState(''); - const [creating, setCreating] = useState(false); - const [error, setError] = useState(null); - - const handleCreate = async () => { - setCreating(true); - setError(null); - try { - await createAndActivateAccount(alias || 'My Account'); - onCreated(); - } catch (err: any) { - setError(err.message); - } finally { - setCreating(false); - } - }; - - return ( -
- {error &&
{error}
} - -
- - setAlias(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleCreate()} - placeholder="My Account" - disabled={creating} - /> -
- - -
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx deleted file mode 100644 index 610405811bb1..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/Header.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; - -import { getOriginHost } from '../utils'; -import type { ConnectedSite } from './types'; - -export function Header({ pendingCount, onApprovalClick, connectedSites, onDisconnect, onSettingsClick }: { - pendingCount: number; - onApprovalClick: () => void; - connectedSites: ConnectedSite[]; - onDisconnect: (sessionId: string) => void; - onSettingsClick: () => void; -}) { - const [showDropdown, setShowDropdown] = useState(false); - const dropdownRef = useRef(null); - const connected = connectedSites.length > 0; - - // Close dropdown when clicking outside - useEffect(() => { - if (!showDropdown) return; - const handleClick = (e: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { - setShowDropdown(false); - } - }; - document.addEventListener('mousedown', handleClick); - return () => document.removeEventListener('mousedown', handleClick); - }, [showDropdown]); - - return ( -
-

Aztec Wallet

-
- local - -
- - - {showDropdown && connected && ( -
- {connectedSites.map((site) => ( -
- {getOriginHost(site.origin)} - -
- ))} -
- )} -
- - - - -
-
- ); -} - -export function SubHeader({ title, onBack }: { title: string; onBack: () => void }) { - return ( -
- - {title} -
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx deleted file mode 100644 index 16a4677133be..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/LockScreen.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState } from 'react'; - -import { MessageTypes } from '../config'; -import { sendToBackground, waitForTask } from './helpers'; - -export function LockScreen({ onUnlocked }: { onUnlocked: () => void }) { - const [password, setPassword] = useState(''); - const [unlocking, setUnlocking] = useState(false); - const [error, setError] = useState(null); - - const handleUnlock = async () => { - if (!password) return; - setUnlocking(true); - setError(null); - try { - const { taskId } = await sendToBackground({ - type: MessageTypes.UNLOCK_WALLET, - password, - }); - await waitForTask(taskId); - onUnlocked(); - } catch (err: any) { - setError(err.message); - } finally { - setUnlocking(false); - } - }; - - return ( -
-
🔒
-

Wallet Locked

-

- Enter your password to unlock. -

- - {error &&
{error}
} - -
- setPassword(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleUnlock()} - placeholder="Enter password" - disabled={unlocking} - /> -
- - -
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx deleted file mode 100644 index d6f8debe060b..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/SettingsPage.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useState, useRef } from 'react'; - -import { MessageTypes, AZTEC_PACKAGES_VERSION } from '../config'; -import type { WalletExportData } from '../shared-types'; -import { sendToBackground, waitForTask } from './helpers'; - -export function SettingsPage({ onImportStart }: { onImportStart: (data: WalletExportData) => void }) { - const [exporting, setExporting] = useState(false); - const [importPreview, setImportPreview] = useState(null); - const [error, setError] = useState(null); - const fileInputRef = useRef(null); - - const handleExport = async () => { - setExporting(true); - setError(null); - try { - const { taskId } = await sendToBackground({ type: MessageTypes.EXPORT_WALLET }); - const result = await waitForTask(taskId); - - const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const date = new Date().toISOString().slice(0, 10); - const a = document.createElement('a'); - a.href = url; - a.download = `aztec-wallet-backup-${date}.json`; - a.click(); - URL.revokeObjectURL(url); - } catch (err: any) { - setError(err.message); - } finally { - setExporting(false); - } - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - setError(null); - setImportPreview(null); - - const reader = new FileReader(); - reader.onload = (ev) => { - try { - const data = JSON.parse(ev.target?.result as string) as WalletExportData; - if (data.version !== 1) { - setError('Unsupported backup version'); - return; - } - if (!Array.isArray(data.accounts) || data.accounts.some((a) => !a.address || !a.secret || !a.salt)) { - setError('Invalid backup file: missing account data'); - return; - } - setImportPreview(data); - } catch { - setError('Failed to parse backup file'); - } - }; - reader.readAsText(file); - // Reset input so the same file can be selected again - e.target.value = ''; - }; - - const versionMismatch = importPreview && importPreview.aztecPackagesVersion !== AZTEC_PACKAGES_VERSION; - - return ( -
- {error &&
{error}
} - - {/* Export section */} -
-
Export Wallet Backup
-
- Download a JSON file containing all your accounts with decrypted secrets. - Store this file securely. -
- -
- - {/* Import section */} -
-
Import Wallet Backup
-
- Restore accounts from a previously exported backup file. -
- - - - - - {importPreview && ( -
-
Backup Preview
-
- Accounts - {importPreview.accounts.length} -
-
- Aztec Version - {importPreview.aztecPackagesVersion} -
-
- Exported - {new Date(importPreview.exportedAt).toLocaleDateString()} -
- - {versionMismatch && ( -
- Version mismatch! This backup was created with {importPreview.aztecPackagesVersion} but - the current wallet uses {AZTEC_PACKAGES_VERSION}. Imported addresses may not match. -
- )} - -
- This will wipe your current wallet. You will need to set a new master password. -
- - -
- )} -
- - {/* Version info */} -
- Aztec Packages Version - {AZTEC_PACKAGES_VERSION} -
-
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx deleted file mode 100644 index 9a27d42df459..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/SetupScreen.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState } from 'react'; - -import { MessageTypes } from '../config'; -import { sendToBackground, waitForTask, createAndActivateAccount } from './helpers'; - -export function SetupScreen({ onComplete, skipAccountCreation = false }: { onComplete: () => void; skipAccountCreation?: boolean }) { - const [step, setStep] = useState<'password' | 'account'>('password'); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [alias, setAlias] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSetPassword = async () => { - if (password.length < 8) { - setError('Password must be at least 8 characters'); - return; - } - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - setLoading(true); - setError(null); - try { - const { taskId } = await sendToBackground({ type: MessageTypes.SETUP_PASSWORD, password }); - await waitForTask(taskId); - if (skipAccountCreation) { - onComplete(); - return; - } - setStep('account'); - } catch (err: any) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - const handleCreateFirstAccount = async () => { - setLoading(true); - setError(null); - try { - await createAndActivateAccount(alias || 'Account 1'); - onComplete(); - } catch (err: any) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - if (step === 'password') { - return ( -
-
🔒
-

Welcome to Aztec Wallet

-

- Set a master password to protect your accounts. -

- - {error &&
{error}
} - -
- setPassword(e.target.value)} - placeholder="Password (min 8 characters)" - disabled={loading} - /> -
- -
- setConfirmPassword(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSetPassword()} - placeholder="Confirm password" - disabled={loading} - /> -
- - -
- ); - } - - return ( -
-
👤
-

Create Your First Account

-

- Choose a name for your first Aztec account. -

- - {error &&
{error}
} - -
- setAlias(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleCreateFirstAccount()} - placeholder="Account name (e.g. Account 1)" - disabled={loading} - /> -
- - -
- ); -} diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts b/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts deleted file mode 100644 index 252c5d3a22a4..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/helpers.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { MessageTarget, MessageTypes } from '../config'; -import type { BackgroundTask } from '../shared-types'; - -// docs:start:send-message -/** - * Sends a message to the background script via chrome.runtime.sendMessage. - * Used for simple request/response calls (accounts, status, approvals). - */ -export function sendToBackground(message: any): Promise { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - { ...message, target: MessageTarget.BACKGROUND }, - (response) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - return; - } - if (response?.success) { - resolve(response.result); - } else { - reject(new Error(response?.error || 'Unknown error')); - } - } - ); - }); -} -// docs:end:send-message - -/** - * Registry of pending task promises. (#12) - * - * When the background pushes a task-update via the port, we resolve/reject - * the corresponding promise. If the popup was closed and reopened, the initial - * state push includes all recent tasks — we process completed ones immediately - * so waitForTask resolves even if the task finished while we were closed. - */ -export const pendingTaskCallbacks = new Map void; - reject: (error: Error) => void; -}>(); - -export function waitForTask(taskId: string, timeoutMs = 300000): Promise { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - pendingTaskCallbacks.delete(taskId); - reject(new Error('Task timed out')); - }, timeoutMs); - - pendingTaskCallbacks.set(taskId, { - resolve: (value: any) => { - clearTimeout(timeoutId); - pendingTaskCallbacks.delete(taskId); - resolve(value); - }, - reject: (error: Error) => { - clearTimeout(timeoutId); - pendingTaskCallbacks.delete(taskId); - reject(error); - }, - }); - }); -} - -export function handleTaskUpdate(task: BackgroundTask) { - const callbacks = pendingTaskCallbacks.get(task.id); - if (!callbacks) return; - - if (task.status === 'success') { - callbacks.resolve(task.result); - } else if (task.status === 'error') { - callbacks.reject(new Error(task.error || 'Task failed')); - } -} - -// docs:start:helpers -export function truncateAddress(address: string): string { - if (!address) return ''; - if (address.length <= 16) return address; - return `${address.slice(0, 8)}...${address.slice(-6)}`; -} - -/** - * Creates an account and sets it as active. - * Shared between SetupScreen (first account) and CreateAccountView (additional accounts). - */ -export async function createAndActivateAccount(alias: string): Promise { - const { taskId } = await sendToBackground({ - type: MessageTypes.CREATE_ACCOUNT, - alias, - }); - const result = await waitForTask(taskId); - - if (result?.address) { - await sendToBackground({ - type: MessageTypes.SET_ACTIVE_ACCOUNT, - address: result.address, - }); - } -} -// docs:end:helpers diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx b/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx deleted file mode 100644 index f44a5151ceca..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/popup.tsx +++ /dev/null @@ -1,467 +0,0 @@ -/** - * Popup UI for the Aztec Tutorial Wallet. - * - * MetaMask-like layout with: - * - Setup screen (first-time password + first account) - * - Lock screen (unlock with master password) - * - Main page (active account detail, deploy button) - * - Account switcher overlay - * - Create account sub-page - * - Approvals view for connection/transaction requests - * - * Communication: - * - Persistent port to background for real-time push updates (no polling) - * - Port auto-reconnects if background disconnects (#9) - * - Initial state comes from the port, not separate fetches (#10) - */ - -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { createRoot } from 'react-dom/client'; - -import { MessageTypes } from '../config'; -import type { WalletExportData, PublicAccountInfo, PendingTransaction, PendingCapabilities, ConnectedSite, PendingSessionVerification, BackgroundTask, View } from '../shared-types'; -import type { PendingDiscovery } from '@aztec/wallet-sdk/extension/handlers'; -import { sendToBackground, waitForTask, handleTaskUpdate } from './helpers'; -import { Header, SubHeader } from './Header'; -import { SetupScreen } from './SetupScreen'; -import { LockScreen } from './LockScreen'; -import { MainPage } from './MainScreen'; -import { AccountSwitcher } from './AccountSwitcher'; -import { CreateAccountView } from './CreateAccountView'; -import { ApprovalsView, SessionVerificationView } from './ApprovalView'; -import { SettingsPage } from './SettingsPage'; - -// docs:start:main-app -function App() { - const [view, setView] = useState('loading'); - const [accounts, setAccounts] = useState([]); - const [activeAccount, setActiveAccount] = useState(null); - const [discoveries, setDiscoveries] = useState([]); - const [transactions, setTransactions] = useState([]); - const [connectedSites, setConnectedSites] = useState([]); - const [sessionVerifications, setSessionVerifications] = useState([]); - const [pendingCapabilities, setPendingCapabilities] = useState([]); - const [runningTasks, setRunningTasks] = useState([]); - const [error, setError] = useState(null); - const [elapsed, setElapsed] = useState(0); - const [pendingImportData, setPendingImportData] = useState(null); - const portRef = useRef(null); - const reconnectTimerRef = useRef | null>(null); - - const pendingCount = discoveries.length + transactions.length + sessionVerifications.length + pendingCapabilities.length; - - /** - * Applies state pushed from the background via the port. (#10) - * This is the single source of truth for pending items, tasks, and connected sites. - */ - const applyBackgroundState = useCallback((data: any) => { - if (data.discoveries) setDiscoveries(data.discoveries); - if (data.transactions) setTransactions(data.transactions); - if (data.pendingSessionVerifications) setSessionVerifications(data.pendingSessionVerifications); - if (data.pendingCapabilities) setPendingCapabilities(data.pendingCapabilities); - if (data.connectedSites) setConnectedSites(data.connectedSites); - if (data.tasks) { - setRunningTasks(data.tasks.filter((t: BackgroundTask) => t.status === 'running')); - // Resolve any waitForTask promises for completed tasks (#12) - for (const task of data.tasks) { - handleTaskUpdate(task); - } - } - }, []); - - /** - * Loads account data and determines the initial view. - * Pending items come from the port push, NOT from a separate fetch. (#10) - */ - const loadData = useCallback(async () => { - try { - setError(null); - - const [accountsResult, activeAccountResult, statusResult] = await Promise.all([ - sendToBackground({ type: MessageTypes.GET_ACCOUNTS }), - sendToBackground({ type: MessageTypes.GET_ACTIVE_ACCOUNT }), - sendToBackground({ type: MessageTypes.GET_WALLET_STATUS }), - ]); - - setAccounts(accountsResult || []); - setActiveAccount(activeAccountResult || null); - - const unlocked = statusResult?.unlocked || false; - const hasPassword = statusResult?.hasPassword || false; - const hasAccounts = (accountsResult || []).length > 0; - - if (!hasPassword && !hasAccounts) { - setView('setup'); - } else if (!unlocked) { - setView('lock'); - } else { - setView((prev) => prev === 'loading' ? 'main' : prev); - } - } catch (err: any) { - console.error('Failed to load data:', err); - setError(err.message); - setView('setup'); - } - }, []); - - /** - * Connect persistent port to background. (#9) - * Reconnects automatically if the background disconnects (e.g., SW restart). - */ - const connectPort = useCallback(() => { - if (reconnectTimerRef.current) { - clearTimeout(reconnectTimerRef.current); - reconnectTimerRef.current = null; - } - - try { - const port = chrome.runtime.connect({ name: 'popup' }); - portRef.current = port; - - port.onMessage.addListener((message: any) => { - if (message.type === 'state') { - applyBackgroundState(message.data); - - // Auto-navigate to approvals/verification if there are pending items - const d = message.data.discoveries?.length || 0; - const t = message.data.transactions?.length || 0; - const sv = message.data.pendingSessionVerifications?.length || 0; - const c = message.data.pendingCapabilities?.length || 0; - if (sv > 0) { - setView((prev) => (prev === 'main' || prev === 'loading' || prev === 'approvals') ? 'verifySession' : prev); - } else if (d > 0 || t > 0 || c > 0) { - setView((prev) => (prev === 'main' || prev === 'loading') ? 'approvals' : prev); - } - } else if (message.type === 'task-update') { - const task: BackgroundTask = message.task; - handleTaskUpdate(task); - - setRunningTasks((prev) => { - if (task.status === 'running') { - const existing = prev.findIndex((t) => t.id === task.id); - if (existing >= 0) { - const updated = [...prev]; - updated[existing] = task; - return updated; - } - return [...prev, task]; - } - return prev.filter((t) => t.id !== task.id); - }); - - // Refresh account data if a state-changing task completed - if (task.status === 'success') { - const refreshTypes = ['create-account', 'deploy-account', 'unlock', 'setup-password', 'import-wallet-accounts']; - if (refreshTypes.includes(task.type)) { - loadData(); - } - } - } - }); - - port.onDisconnect.addListener(() => { - console.log('[popup] Port disconnected, will reconnect...'); - portRef.current = null; - // Reconnect after a short delay (SW may be restarting) (#9) - reconnectTimerRef.current = setTimeout(connectPort, 1000); - }); - - } catch (err) { - console.error('[popup] Failed to connect port:', err); - // Retry connection (#9) - reconnectTimerRef.current = setTimeout(connectPort, 2000); - } - }, [applyBackgroundState, loadData]); - - useEffect(() => { - connectPort(); - loadData(); - - return () => { - if (reconnectTimerRef.current) { - clearTimeout(reconnectTimerRef.current); - } - if (portRef.current) { - portRef.current.disconnect(); - portRef.current = null; - } - }; - }, [connectPort, loadData]); - - // Reactive auto-navigation: ensures the popup shows the right view whenever - // pending items exist, even if the port message handler's auto-nav fired while - // the popup was on a non-target view (e.g. 'setup' or 'lock'). - useEffect(() => { - if (sessionVerifications.length > 0 && - (view === 'main' || view === 'loading' || view === 'approvals')) { - setView('verifySession'); - } else if ((discoveries.length > 0 || transactions.length > 0 || pendingCapabilities.length > 0) && - (view === 'main' || view === 'loading')) { - setView('approvals'); - } - }, [sessionVerifications, discoveries, transactions, pendingCapabilities, view]); - - // Tick elapsed time while tasks are running - useEffect(() => { - if (runningTasks.length === 0) { - setElapsed(0); - return; - } - const oldest = Math.min(...runningTasks.map((t) => t.startedAt)); - setElapsed(Math.round((Date.now() - oldest) / 1000)); - const timer = setInterval(() => { - setElapsed(Math.round((Date.now() - oldest) / 1000)); - }, 1000); - return () => clearInterval(timer); - }, [runningTasks]); - - const handleUnlocked = () => { - setView('main'); - loadData(); - }; - - const handleSetupComplete = async () => { - if (pendingImportData) { - try { - const { taskId } = await sendToBackground({ - type: MessageTypes.IMPORT_WALLET_ACCOUNTS, - accounts: pendingImportData.accounts, - activeAccount: pendingImportData.activeAccount, - }); - await waitForTask(taskId); - setPendingImportData(null); - } catch (err: any) { - console.error('Failed to import accounts:', err); - setError(err.message); - setPendingImportData(null); - } - } - setView('main'); - loadData(); - }; - - const handleImportStart = (data: WalletExportData) => { - setPendingImportData(data); - sendToBackground({ type: MessageTypes.IMPORT_WALLET }).then(() => { - setView('setup'); - }).catch((err) => { - console.error('Failed to wipe wallet:', err); - setError(err.message); - setPendingImportData(null); - }); - }; - - const handleDisconnectSite = async (sessionId: string) => { - try { - await sendToBackground({ type: MessageTypes.DISCONNECT_SESSION, sessionId }); - } catch (err) { - console.error('Failed to disconnect session:', err); - } - }; - - const handleConfirmSession = async (sessionId: string) => { - try { - await sendToBackground({ type: MessageTypes.CONFIRM_SESSION, sessionId }); - setView('main'); - } catch (err) { - console.error('Failed to confirm session:', err); - } - }; - - const handleRejectSession = async (sessionId: string) => { - try { - await sendToBackground({ type: MessageTypes.REJECT_SESSION, sessionId }); - setView('main'); - } catch (err) { - console.error('Failed to reject session:', err); - } - }; - - const activeAccountData = accounts.find((a) => a.address === activeAccount) || accounts[0] || null; - - const handleApprovalClick = () => { - if (sessionVerifications.length > 0) { - setView('verifySession'); - } else { - setView('approvals'); - } - }; - - const noopDisconnect = () => {}; - - if (view === 'loading') { - return ( -
-
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> -
-
- Loading... -
-
- ); - } - - if (view === 'setup') { - return ( -
-
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> - -
- ); - } - - if (view === 'lock') { - return ( -
-
{}} connectedSites={[]} onDisconnect={noopDisconnect} onSettingsClick={() => {}} /> - -
- ); - } - - if (view === 'approvals') { - return ( -
-
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> - setView('main')} /> - -
- ); - } - - if (view === 'verifySession') { - const currentVerification = sessionVerifications[0]; - return ( -
-
{}} connectedSites={connectedSites} onDisconnect={handleDisconnectSite} onSettingsClick={() => setView('settings')} /> - setView('main')} /> - {currentVerification ? ( - handleConfirmSession(currentVerification.sessionId)} - onReject={() => handleRejectSession(currentVerification.sessionId)} - /> - ) : ( -
-
-
No pending verifications
-
- )} -
- ); - } - - if (view === 'switcher') { - return ( -
-
setView('settings')} /> - setView('main')} /> - { - sendToBackground({ type: MessageTypes.SET_ACTIVE_ACCOUNT, address }) - .then(() => { setActiveAccount(address); setView('main'); }) - .catch((err) => console.error('Failed to switch account:', err)); - }} - onCreateNew={() => setView('createAccount')} - /> -
- ); - } - - if (view === 'createAccount') { - return ( -
-
setView('settings')} /> - setView('switcher')} /> - { setView('main'); loadData(); }} /> -
- ); - } - - if (view === 'settings') { - return ( -
-
{}} /> - setView('main')} /> - -
- ); - } - - // Main view - return ( -
-
setView('settings')} /> - - {error &&
{error}
} - - {runningTasks.length > 0 && ( -
-
-
- {runningTasks.map((t) => { - const labels: Record = { - 'deploy-account': 'Deploying account...', - 'create-account': 'Creating account...', - 'unlock': 'Unlocking wallet...', - 'setup-password': 'Setting up password...', - 'export-wallet': 'Exporting wallet...', - 'import-wallet-accounts': 'Importing accounts...', - }; - const genericLabel = t.type.startsWith('wallet:') - ? `Processing ${t.type.replace('wallet:', '')}...` - : t.type.startsWith('tx:') - ? `Executing ${t.type.replace('tx:', '')}...` - : labels[t.type] || 'Processing...'; - return ( -
-
{t.progress || genericLabel}
- {t.progress && ( -
{genericLabel}
- )} -
- ); - })} -
- Elapsed: {Math.floor(elapsed / 60)}:{String(elapsed % 60).padStart(2, '0')} -
-
-
- )} - - {activeAccountData ? ( - 0} - onSwitcherOpen={() => setView('switcher')} - onRefresh={loadData} - /> - ) : ( -
-
👛
-
No accounts yet
- -
- )} -
- ); -} -// docs:end:main-app - -// Mount the app -const root = createRoot(document.getElementById('root')!); -root.render(); diff --git a/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts b/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts deleted file mode 100644 index 16e1e8f4ab46..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/popup/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Re-export shared types for the popup. - * The canonical definitions live in shared-types.ts — import from there - * or from this module (which re-exports everything the popup needs). - */ -export type { - PublicAccountInfo, - PendingTransaction, - PendingSessionVerification, - PendingCapabilities, - ConnectedSite, - BackgroundTask, - WalletExportData, - View, -} from '../shared-types'; - -/** Alias for popup components that display account info. */ -export type { PublicAccountInfo as StoredAccount } from '../shared-types'; diff --git a/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts b/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts deleted file mode 100644 index 4f8cc4c4448a..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/shared-types.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Shared type definitions for the Aztec Tutorial Wallet extension. - * Single source of truth for types used across background, offscreen, and popup. - */ - -/** - * Public account info — the subset of account data safe to expose to the UI. - * Does NOT include encrypted secrets, IVs, or contract salts. - */ -export interface PublicAccountInfo { - address: string; - alias: string; - isDeployed: boolean; -} - -/** - * Pending transaction awaiting user approval. - */ -export interface PendingTransaction { - sessionId: string; - messageId: string; - method: string; - args: any; - from: string; - origin: string; - timestamp: number; -} - -/** - * Session pending emoji verification. - * After key exchange completes, we hold the session here until - * the user confirms the verification emojis match. - */ -export interface PendingSessionVerification { - sessionId: string; - origin: string; - appId: string; - verificationHash: string; - timestamp: number; -} - -/** - * Pending capability request awaiting user approval. - */ -export interface PendingCapabilities { - sessionId: string; - messageId: string; - origin: string; - /** App metadata from the capability manifest */ - appMetadata: { - name: string; - version: string; - description?: string; - url?: string; - icon?: string; - }; - /** Raw requested capabilities array */ - capabilities: Array<{ type: string; [key: string]: any }>; - timestamp: number; -} - -/** Connected site info for display in the popup. */ -export interface ConnectedSite { - sessionId: string; - origin: string; - appId: string; - connectedAt: number; -} - -/** Background task for long-running operations. */ -export interface BackgroundTask { - id: string; - type: string; - status: 'running' | 'success' | 'error'; - progress?: string; - result?: any; - error?: string; - startedAt: number; -} - -/** Wallet export data format for backup/restore. */ -export interface WalletExportData { - version: 1; - aztecPackagesVersion: string; - exportedAt: string; - accounts: Array<{ - address: string; - secret: string; - salt: string; - alias: string; - isDeployed: boolean; - }>; - activeAccount: string | null; -} - -/** Popup view state. */ -export type View = 'loading' | 'setup' | 'lock' | 'main' | 'switcher' | 'createAccount' | 'approvals' | 'verifySession' | 'settings'; diff --git a/docs/examples/webapp-tutorial/test-extension/src/utils.ts b/docs/examples/webapp-tutorial/test-extension/src/utils.ts deleted file mode 100644 index a6d5d42a4b00..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Shared utilities for the Aztec Tutorial Wallet extension. - * Extracted from multiple files to eliminate duplication. - */ - -/** - * Resolves the real Chrome runtime object. - * Needed in offscreen documents where polyfills may shadow the global. - */ -export function getChromeRuntime(): typeof chrome { - const candidates = [ - typeof self !== 'undefined' ? (self as any).chrome : undefined, - typeof window !== 'undefined' ? (window as any).chrome : undefined, - (globalThis as any).chrome, - ]; - - for (const candidate of candidates) { - if (candidate?.runtime?.sendMessage) { - return candidate; - } - } - - throw new Error('Chrome runtime API not available'); -} - -/** - * Extracts a human-readable error message from any thrown value. - */ -export function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); -} - -/** - * Extracts the hostname from an origin URL, falling back to the raw string. - */ -export function getOriginHost(origin: string): string { - try { - return new URL(origin).host; - } catch { - return origin; - } -} - diff --git a/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts b/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts deleted file mode 100644 index 703afdc13832..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/wallet/storage.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * Encrypted key storage for the Aztec Tutorial Wallet. - * - * Security design: - * - Master password is NEVER stored or cached as a string (#2) - * - Password verification uses PBKDF2 + AES-GCM (encrypt known plaintext) (#1) - * - A non-extractable CryptoKey is derived once at unlock and cached in memory - * - All account secrets are encrypted with this master CryptoKey - * - Each account gets a unique random IV for AES-GCM - * - * WARNING: This is for tutorial purposes. Production wallets should use - * hardware security modules, secure enclaves, or platform keychain APIs. - */ - -/** Known plaintext used to verify the master password is correct */ -const VERIFICATION_PLAINTEXT = 'aztec-wallet-verify'; - -/** - * PBKDF2 iteration count. - * OWASP 2023 recommends >= 600,000 for SHA-256. - * Higher = slower brute force, but also slower unlock. - */ -const PBKDF2_ITERATIONS = 600_000; - -/** Stored account data structure */ -export interface StoredAccount { - /** Aztec address as hex string */ - address: string; - /** Encrypted secret key (base64 encoded) */ - encryptedSecret: string; - /** IV for AES-GCM (base64 encoded) — unique per account */ - iv: string; - /** User-friendly alias */ - alias: string; - /** Whether the account contract is deployed */ - isDeployed: boolean; - /** Account contract salt as hex string */ - contractSalt: string; -} - -/** Storage keys — shared with background.ts for direct chrome.storage access. */ -export const STORAGE_KEYS = { - ACCOUNTS: 'aztec_accounts', - PASSWORD_DATA: 'aztec_password_data', - ACTIVE_ACCOUNT: 'aztec_active_account', -} as const; - -/** Encode bytes to base64 */ -function bytesToBase64(bytes: Uint8Array): string { - return btoa(String.fromCharCode(...bytes)); -} - -/** Decode base64 to bytes */ -function base64ToBytes(b64: string): Uint8Array { - return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); -} - -// docs:start:derive-master-key -/** - * Derives a non-extractable AES-GCM CryptoKey from a password and salt using PBKDF2. - * - * The key is non-extractable: once created, the raw key material cannot be read - * from JavaScript. This means even if an attacker has a reference to the CryptoKey - * object, they cannot extract the underlying bytes. - */ -export async function deriveMasterKey( - password: string, - salt: Uint8Array -): Promise { - const encoder = new TextEncoder(); - const passwordKey = await crypto.subtle.importKey( - 'raw', - encoder.encode(password), - 'PBKDF2', - false, - ['deriveKey'] - ); - - return crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt, - iterations: PBKDF2_ITERATIONS, - hash: 'SHA-256', - }, - passwordKey, - { name: 'AES-GCM', length: 256 }, - false, // non-extractable - ['encrypt', 'decrypt'] - ); -} -// docs:end:derive-master-key - -// docs:start:encrypt-decrypt -/** - * Encrypts a secret using a CryptoKey. - * Each call generates a fresh random IV for AES-GCM. - */ -export async function encryptWithKey( - secret: string, - key: CryptoKey -): Promise<{ encrypted: string; iv: string }> { - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encoder = new TextEncoder(); - - const ciphertext = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - key, - encoder.encode(secret) - ); - - return { - encrypted: bytesToBase64(new Uint8Array(ciphertext)), - iv: bytesToBase64(iv), - }; -} - -/** - * Decrypts a secret using a CryptoKey. - */ -export async function decryptWithKey( - encrypted: string, - iv: string, - key: CryptoKey -): Promise { - const ivBytes = base64ToBytes(iv); - const ciphertextBytes = base64ToBytes(encrypted); - - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv: ivBytes }, - key, - ciphertextBytes - ); - - return new TextDecoder().decode(decrypted); -} -// docs:end:encrypt-decrypt - -import { getChromeRuntime } from '../utils'; - -/** - * Chrome runtime for messaging (available in offscreen documents). - * Offscreen documents don't have direct chrome.storage access, - * so all storage operations are proxied through the background script. - */ - -/** - * Storage operations are proxied through the background script because - * offscreen documents have limited chrome API access (no chrome.storage). - */ -const STORAGE_PROXY_TIMEOUT_MS = 30_000; // 30 seconds - -async function storageGet(key: string): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); - getChromeRuntime().runtime.sendMessage( - { type: 'storage-get', key }, - (response: any) => { - clearTimeout(timer); - if (response?.success) { - resolve(response.result); - } else { - reject(new Error(response?.error || 'Storage get failed')); - } - } - ); - }); -} - -async function storageSet(data: Record): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error('Storage proxy timed out')), STORAGE_PROXY_TIMEOUT_MS); - getChromeRuntime().runtime.sendMessage( - { type: 'storage-set', data }, - (response: any) => { - clearTimeout(timer); - if (response?.success) { - resolve(); - } else { - reject(new Error(response?.error || 'Storage set failed')); - } - } - ); - }); -} - -// docs:start:password-management -/** - * Sets the master password for the first time. (#1) - * - * Instead of storing an unsalted SHA-256 hash (vulnerable to rainbow tables), - * we derive a CryptoKey via PBKDF2 with a random salt, then encrypt a known - * plaintext. Verification = re-derive key + try to decrypt. - * - * Returns the derived master CryptoKey so the caller can cache it immediately. - */ -export async function setupPassword(password: string): Promise { - const salt = crypto.getRandomValues(new Uint8Array(32)); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const masterKey = await deriveMasterKey(password, salt); - - const encoder = new TextEncoder(); - const ciphertext = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - masterKey, - encoder.encode(VERIFICATION_PLAINTEXT) - ); - - await storageSet({ - [STORAGE_KEYS.PASSWORD_DATA]: { - salt: bytesToBase64(salt), - iv: bytesToBase64(iv), - verifier: bytesToBase64(new Uint8Array(ciphertext)), - }, - }); - - return masterKey; -} - -/** - * Verifies the password and returns the derived master CryptoKey. (#1, #2) - * - * If the password is correct, returns the non-extractable CryptoKey. - * If wrong, returns null (AES-GCM decryption fails with wrong key). - * The caller should cache the CryptoKey and discard the password string. - */ -export async function verifyAndDeriveMasterKey(password: string): Promise { - const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); - if (!data) return null; - - const salt = base64ToBytes(data.salt); - const iv = base64ToBytes(data.iv); - const verifier = base64ToBytes(data.verifier); - - const masterKey = await deriveMasterKey(password, salt); - - try { - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - masterKey, - verifier - ); - const decoded = new TextDecoder().decode(decrypted); - if (decoded === VERIFICATION_PLAINTEXT) { - return masterKey; - } - return null; - } catch { - // AES-GCM decryption throws on wrong key (authentication tag mismatch) - return null; - } -} - -/** - * Checks if a master password has been set. - */ -export async function hasPassword(): Promise { - const data = await storageGet(STORAGE_KEYS.PASSWORD_DATA); - return !!data; -} -// docs:end:password-management - -// docs:start:account-operations -/** - * Saves an account to storage. - */ -export async function saveAccount(account: StoredAccount): Promise { - const accounts = await getStoredAccounts(); - const existingIndex = accounts.findIndex((a) => a.address === account.address); - - if (existingIndex >= 0) { - accounts[existingIndex] = account; - } else { - accounts.push(account); - } - - await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); -} - -/** - * Retrieves all stored accounts. - */ -export async function getStoredAccounts(): Promise { - const result = await storageGet(STORAGE_KEYS.ACCOUNTS); - return result || []; -} - -/** - * Gets a specific account by address. - */ -export async function getStoredAccount( - address: string -): Promise { - const accounts = await getStoredAccounts(); - return accounts.find((a) => a.address === address); -} - -/** - * Updates an account's deployment status. - */ -export async function markAccountDeployed(address: string): Promise { - const accounts = await getStoredAccounts(); - const account = accounts.find((a) => a.address === address); - if (account) { - account.isDeployed = true; - await storageSet({ [STORAGE_KEYS.ACCOUNTS]: accounts }); - } -} - -/** - * Removes an account from storage. - */ -export async function removeAccount(address: string): Promise { - const accounts = await getStoredAccounts(); - const filtered = accounts.filter((a) => a.address !== address); - await storageSet({ [STORAGE_KEYS.ACCOUNTS]: filtered }); -} - -/** - * Gets the active account address. - */ -export async function getActiveAccount(): Promise { - const result = await storageGet(STORAGE_KEYS.ACTIVE_ACCOUNT); - return result || null; -} - -/** - * Sets the active account address. - */ -export async function setActiveAccount(address: string): Promise { - await storageSet({ [STORAGE_KEYS.ACTIVE_ACCOUNT]: address }); -} -// docs:end:account-operations diff --git a/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts b/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts deleted file mode 100644 index 2d1bd3637d16..000000000000 --- a/docs/examples/webapp-tutorial/test-extension/src/wallet/wallet-impl.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Wallet implementation for the Aztec Tutorial Wallet extension. - * - * With the crossOriginIsolated monkey-patch, Barretenberg WASM works in Chrome - * extension offscreen documents. This allows full cryptographic operations: - * - Fr.random() for secure random field elements - * - deriveKeys() and deriveSigningKey() for key derivation - * - Address computation with SchnorrAccountContract - * - * All encryption uses a non-extractable CryptoKey — the raw password - * is never stored or passed around after initial derivation. (#2) - */ - -import { Fr } from '@aztec/aztec.js/fields'; - -import { instantiateAccount } from '../account-utils'; -import type { PublicAccountInfo } from '../shared-types'; -import { - type StoredAccount, - getStoredAccounts, - getStoredAccount, - saveAccount, - encryptWithKey, - decryptWithKey, - markAccountDeployed, - getActiveAccount, - setActiveAccount, -} from './storage'; -import { log } from '../config'; - -/** - * Generates a new secret and salt for account creation using real Aztec primitives. - * Uses Fr.random() which is cryptographically secure via Barretenberg. - */ -export function generateSecret(): { secret: string; salt: string } { - log.debug('[wallet-manager] Generating new secret and salt with Fr.random()...'); - const secret = Fr.random().toString(); - const salt = Fr.random().toString(); - return { secret, salt }; -} - -/** - * Computes the account address from a secret and salt. - * Runs in the extension offscreen document where Barretenberg works. - */ -export async function computeAddress(secretHex: string, saltHex: string): Promise { - log.debug('[wallet-manager] Computing address...'); - const { instance } = await instantiateAccount(secretHex, saltHex); - log.debug('[wallet-manager] Computed address:', instance.address.toString()); - return instance.address.toString(); -} - -/** - * Creates a complete account: generates secret/salt, computes address, and stores encrypted. - * Takes a CryptoKey (not a password string) for encryption. (#2) - */ -// docs:start:create-new-account -export async function createAccount( - masterKey: CryptoKey, - alias: string = '' -): Promise<{ address: string; secret: string; salt: string }> { - log.debug('[wallet-manager] Creating account...'); - - const { secret, salt } = generateSecret(); - const address = await computeAddress(secret, salt); - await storeAccount(address, secret, salt, masterKey, alias); - - // Auto-set as active if this is the first account - const currentActive = await getActiveAccount(); - if (!currentActive) { - await setActiveAccount(address); - log.debug('[wallet-manager] Set as active account (first account)'); - } - - log.debug('[wallet-manager] Account created:', address); - return { address, secret, salt }; -} -// docs:end:create-new-account - -/** - * Stores an account with encrypted secret. - * Uses the master CryptoKey for AES-GCM encryption with a per-account random IV. - */ -export async function storeAccount( - address: string, - secret: string, - salt: string, - masterKey: CryptoKey, - alias: string = '' -): Promise { - log.debug('[wallet-manager] Storing account:', address); - - const { encrypted, iv } = await encryptWithKey(secret, masterKey); - - const storedAccount: StoredAccount = { - address, - encryptedSecret: encrypted, - iv, - alias, - isDeployed: false, - contractSalt: salt, - }; - - await saveAccount(storedAccount); - log.debug('[wallet-manager] Account stored successfully'); -} - -/** - * Gets the decrypted secret for an account. - * Takes a CryptoKey (not a password string). (#2) - */ -export async function getAccountSecret( - address: string, - masterKey: CryptoKey -): Promise<{ secret: string; salt: string } | null> { - const stored = await getStoredAccount(address); - if (!stored) { - return null; - } - - const secret = await decryptWithKey( - stored.encryptedSecret, - stored.iv, - masterKey - ); - - return { - secret, - salt: stored.contractSalt, - }; -} - -/** - * Gets all stored accounts (without secrets). - */ -export async function getAccounts(): Promise { - const accounts = await getStoredAccounts(); - return accounts.map(acc => ({ - address: acc.address, - alias: acc.alias, - isDeployed: acc.isDeployed, - })); -} - -/** - * Marks an account as deployed. - */ -export async function markDeployed(address: string): Promise { - await markAccountDeployed(address); -} - -// Re-export storage functions for convenience -export { getActiveAccount, setActiveAccount }; From a47247d2a1190070c765d896f56a1560a264cd62 Mon Sep 17 00:00:00 2001 From: josh crites Date: Tue, 21 Apr 2026 20:31:11 +0000 Subject: [PATCH 13/14] cherry-pick: docs: add Alpha Network page and update privacy/limitations docs (#22515) Original PR: https://github.com/AztecProtocol/aztec-packages/pull/22515 Merge commit: 63c67a0f445eeb037af9249fcc91cb0d7e9da287 Conflicts (left AS-IS with markers, resolved in next commit): docs/CLAUDE.md docs/docs/networks.md --- docs/CLAUDE.md | 52 +++++-- .../resources/considerations/limitations.md | 46 ++----- .../considerations/privacy_considerations.md | 10 +- docs/docs-participate/alpha.md | 129 ++++++++++++++++++ docs/docs-participate/index.md | 4 + docs/docs/networks.md | 4 + docs/sidebars-participate.js | 1 + 7 files changed, 198 insertions(+), 48 deletions(-) create mode 100644 docs/docs-participate/alpha.md diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index d6a4d22e2571..4170cbc60f2b 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -52,14 +52,14 @@ For development: The preprocessing system uses these environment variables: -| Variable | Description | Default | -| -------------- | ------------------------------------------------------------------- | ---------------------------------------- | -| `RELEASE_TYPE` | Release type: `nightly`, `devnet`, `testnet`, `mainnet` | `nightly` | -| `NIGHTLY_TAG` | Version for nightly builds (falls back to `COMMIT_TAG`) | from `developer_version_config.json` | -| `DEVNET_TAG` | Version for devnet builds | from `developer_version_config.json` | -| `TESTNET_TAG` | Version for testnet builds | from `developer_version_config.json` | -| `MAINNET_TAG` | Version for mainnet builds | from `developer_version_config.json` | -| `COMMIT_TAG` | Legacy variable, used as fallback for `NIGHTLY_TAG` | `next` | +| Variable | Description | Default | +| -------------- | ------------------------------------------------------- | ------------------------------------ | +| `RELEASE_TYPE` | Release type: `nightly`, `devnet`, `testnet`, `mainnet` | `nightly` | +| `NIGHTLY_TAG` | Version for nightly builds (falls back to `COMMIT_TAG`) | from `developer_version_config.json` | +| `DEVNET_TAG` | Version for devnet builds | from `developer_version_config.json` | +| `TESTNET_TAG` | Version for testnet builds | from `developer_version_config.json` | +| `MAINNET_TAG` | Version for mainnet builds | from `developer_version_config.json` | +| `COMMIT_TAG` | Legacy variable, used as fallback for `NIGHTLY_TAG` | `next` | ### Preprocessing Macros @@ -163,6 +163,7 @@ The `examples/` directory contains runnable code examples that are included in d - **`AZTEC_NODE_URL`**: All example `index.ts` files and `run.sh` use this env var (defaults to `http://localhost:8080`). In Docker Compose, it points to `http://local-network:8080`. When adding new TypeScript examples: + 1. Create a directory under `examples/ts/` with `index.ts`, `config.yaml`, and empty `yarn.lock` 2. Use `process.env.AZTEC_NODE_URL ?? "http://localhost:8080"` for the node URL 3. Add the example to the list in `examples/ts/aztecjs_runner/run.sh` if it should be executed at runtime @@ -246,6 +247,31 @@ Use these terms consistently throughout: - **Emphasis**: Use _italics_ sparingly for emphasis - **File paths**: Always use forward slashes (e.g., `/usr/local/bin`) - **Placeholders**: Use `[PLACEHOLDER_NAME]` format in examples +- **Em-dashes (`—`)**: Do not use em-dashes. They often signal AI-generated prose and add friction when editing across tools. Rewrite with a comma, colon, parentheses, or a new sentence. Examples: + - ❌ `Alpha is live — bugs are expected.` + - ✅ `Alpha is live, and bugs are expected.` + - ❌ `Limits apply — number of notes, nullifiers, logs.` + - ✅ `Limits apply: number of notes, nullifiers, logs.` + - ❌ `[Networks Overview](/networks) — Technical details` + - ✅ `[Networks Overview](/networks): Technical details` + +### Heading Capitalization + +**Use sentence case for all headings (H1 through H6).** Only capitalize the first word and proper nouns. Do not capitalize common nouns, verbs, prepositions, articles, or conjunctions. + +**Examples:** + +- ✅ `## What Alpha is` +- ✅ `## Known limitations and expected issues` +- ✅ `### Proving system bugs` +- ✅ `## State migration and rollup upgrades` +- ✅ `## Path to beta` +- ✅ `## How to deploy a contract` +- ❌ `## What Alpha Is` +- ❌ `## Known Limitations and Expected Issues` +- ❌ `## How To Deploy A Contract` + +**Applying this to existing files:** When editing an existing page that uses Title Case, convert the headings you touch (and ideally the whole page) to sentence case. The goal is to converge on one style across the site, rather than preserving historical inconsistency. ### Standard Sections @@ -288,7 +314,8 @@ The description should: - ✅ Missing context or assumptions about user knowledge - ✅ Outdated screenshots or version references - ✅ Broken markdown formatting -- ✅ Inconsistent capitalization in headings +- ✅ Headings using Title Case instead of sentence case (see "Heading Capitalization") +- ✅ Em-dashes (`—`) in prose (see "Formatting Conventions") - ✅ Missing alt text for images - ✅ Security implications of commands or configurations @@ -299,7 +326,7 @@ The description should: - ❌ Legal disclaimers or license text - ❌ Direct quotes from external sources - ❌ API endpoint URLs or configuration values -- ❌ Existing migration notes in `resources/migration_notes.md` — never modify already-published migration entries. Instead, add new migration notes to the `## TBD` section at the top of the file. +- ❌ Existing migration notes in `resources/migration_notes.md`. Never modify already-published migration entries. Instead, add new migration notes to the `## TBD` section at the top of the file. ## Review Output Format @@ -355,5 +382,10 @@ Approved external documentation sources: - Suggest improvements even if they go beyond pure editing - When making changes to documentation processes or tooling, remember to check and update READMEs, project documentation (like this file), and code comments +<<<<<<< HEAD Last updated: 2026-02-23 Version: 1.5 +======= +Last updated: 2026-04-21 +Version: 1.7 +>>>>>>> 63c67a0f44 (docs: add Alpha Network page and update privacy/limitations docs (#22515)) diff --git a/docs/docs-developers/docs/resources/considerations/limitations.md b/docs/docs-developers/docs/resources/considerations/limitations.md index a23fa6df79a5..81454f0e7ec6 100644 --- a/docs/docs-developers/docs/resources/considerations/limitations.md +++ b/docs/docs-developers/docs/resources/considerations/limitations.md @@ -29,12 +29,12 @@ Help shape and define: ## Limitations developers need to know about -- It is a testing environment, insecure and unaudited. It is only for testing purposes. -- `msg_sender` is currently leaked when making private -> public calls. - - The `msg_sender` is always set. If you call a public function from the private world, the `msg_sender` is set to the private caller's address. - - There are patterns that can mitigate this. +- The Aztec stack is unaudited and under active development. See the [Alpha Network](/participate/alpha) page for details on what this means. +- `msg_sender` is leaked by default when making private -> public calls. + - `self.enqueue(...)` sets `msg_sender` to the private caller's address, which is publicly visible. + - Use `self.enqueue_incognito(...)` to hide the sender. The called public function must use `maybe_msg_sender()` instead of `msg_sender()` to handle the null sender. - The initial `msg_sender` is `-1`, which can be problematic for some contracts. -- The number of side-effects attached to a transaction (when sending the transaction to the mempool) is leaky. At this stage of development, this is _intentional_, so that we can gauge appropriate choices for privacy sets. We have clear plans to implement privacy sets so that side effects are much less leaky, and these will be in place for mainnet. +- Some side-effect counts are still visible in a transaction. Note hashes, nullifiers, and private logs are padded to hide their true counts, but the number of public function calls and L2->L1 messages remains visible. Privacy sets to further reduce leakage are still under development. - A transaction can only emit a limited number of side-effects (notes, nullifiers, logs, L2->L1 messages). See [circuit limitations](#circuit-limitations). - We have not settled on the final constants, since we are still in a testing phase. You could find that certain compositions of nested private function calls (for example, call stacks that are dynamic in size, based on runtime data) could accumulate so many side-effects as to exceed transaction limits. Such transactions would then be unprovable. Please open an issue if you encounter this, as it will help us decide on adequate sizes for our constants. - Not all Noir cryptographic primitives work in public (AVM) functions. Signature verification (ECDSA secp256k1/r1), AES-128, Blake2s, and Blake3 are not supported. See [AVM Cryptographic Compatibility](../../foundational-topics/advanced/circuits/avm_compatibility.md) for details and workarounds. @@ -42,7 +42,7 @@ Help shape and define: ## WARNING -Do not use real, meaningful secrets in Aztec testnets. Some privacy features are still in development, including ensuring a secure "zk" property. Since the Aztec stack is still being developed, there are no guarantees that real secrets will remain secret. +Do not use real, meaningful secrets on Aztec networks. Some privacy features are still in development, including ensuring a secure "zk" property. Since the Aztec stack is still being developed, there are no guarantees that real secrets will remain secret. ## Limitations @@ -60,15 +60,11 @@ Some of our more complex circuits are still in development, so they are still un Sound proofs are really only needed as a protection against malicious behavior, which we are not testing for at this stage. -### Keys and addresses are subject to change +### Keys and addresses may change in future rollup versions -The way in which keypairs and addresses are derived is still being iterated on as we receive feedback. +The key derivation scheme is documented and stable within the current rollup version, but it may change in future rollup upgrades. Applications should not hardcode assumptions about the specific derivation algorithm. -#### What are the consequences? - -This will impact the kinds of apps that you can build with the local network as it is today. - -Please open new discussions on [Discourse](https://discourse.aztec.network) or open issues on [GitHub](https://github.com/AztecProtocol/aztec-packages) if you have requirements that are not yet being met by the local network's current key derivation scheme. +Please open new discussions on [Discourse](https://discourse.aztec.network) or open issues on [GitHub](https://github.com/AztecProtocol/aztec-packages) if you have requirements that are not being met by the current key derivation scheme. ### No privacy-preserving queries to nodes @@ -76,25 +72,17 @@ Ethereum has a notion of a "full node" which keeps up with the blockchain and st This pattern is likely to develop in Aztec as well, except there is a problem: privacy. If a privacy-seeking user makes a query to a third-party full node, that user might leak data about who they are, about their historical network activity, or about their future intentions. One solution to this problem is "always run a full node", but pragmatically, not everyone will. To protect less-advanced users' privacy, research is underway to explore how a privacy-seeking user may request and receive data from a third-party node without revealing what that data is, nor who is making the request. -### No private data authentication - -Private data should not be returned to an app unless the user authorizes such access to the app. An authorization layer is not yet in place. - -#### What are the consequences? - -Any app can request and receive any private user data relating to any other private app. This sounds problematic, but the local network is a sandbox, and no meaningful value or credentials should be stored there - only test values and test credentials. +### Limited private data authentication -An authorization layer will be added in due course. +The PXE supports a `scopes` parameter that restricts which accounts' notes a function call can access. However, this is caller-specified: the app chooses its own scopes. There is no mandatory, protocol-enforced authorization layer where the PXE denies an app access to another app's private data. A wallet can restrict scope on behalf of the user, but this is not yet standardized or enforced by default. -### No bytecode validation +### No client-side bytecode validation -For safety reasons, bytecode should not be executed unless the PXE (Private eXecution Environment) or wallet has validated that the user's intentions (the function signature and contract address) match the bytecode. +Public bytecode is validated at the protocol level when contract classes are registered (the Contract Class Registry verifies encoding and commitments). However, the PXE and wallets do not yet validate that the bytecode a user is about to execute matches their stated intentions (function signature and contract address). #### What are the consequences? -Without bytecode validation, if incorrect bytecode is executed and that bytecode is malicious, it could read private data from some other contract and emit that private data to the world. This would be problematic in production, but the local network is a sandbox, and no meaningful value or credentials should be stored there - only test values and test credentials. - -There are plans to add bytecode validation soon. +If incorrect or malicious bytecode is executed, it could read private data from another contract and emit it publicly. Client-side bytecode validation is planned to close this gap. ### Insecure hashes @@ -102,11 +90,7 @@ We are planning a full assessment of the protocol's hashes, including rigorous d #### What are the consequences? -Collisions and other hash-related attacks might be possible in the local network. This would be problematic in production, but it is unlikely to cause problems at this early stage of testing. - -### `msg_sender` is leaked when making a private -> public call - -There are ongoing discussions [here](https://forum.aztec.network/t/what-is-msg-sender-when-calling-private-public-plus-a-big-foray-into-stealth-addresses/7527) (and some more recent discussions that need to be documented) around how to address this. +Collisions and other hash-related attacks might be possible. This is unlikely to cause problems at this early stage, but is a known area of ongoing work. ### New privacy standards are required diff --git a/docs/docs-developers/docs/resources/considerations/privacy_considerations.md b/docs/docs-developers/docs/resources/considerations/privacy_considerations.md index c05815da079b..24d85a5cf737 100644 --- a/docs/docs-developers/docs/resources/considerations/privacy_considerations.md +++ b/docs/docs-developers/docs/resources/considerations/privacy_considerations.md @@ -45,7 +45,7 @@ There are many caveats to the above. Since Aztec also enables interaction with t Any time a private function makes a call to a public function, information is leaked. Now, that might be perfectly fine in some use cases (it's up to the smart contract developer). Indeed, most interesting apps will require some public state. But let's have a look at some leaky patterns: - Calling a public function from a private function. The public function execution will be publicly visible. -- Calling a public function from a private function and revealing the `msg_sender` of that call (the `msg_sender` will be publicly visible). +- Calling a public function from a private function and revealing the `msg_sender` of that call (the `msg_sender` will be publicly visible). You can hide the sender by using `self.enqueue_incognito(...)` instead of `self.enqueue(...)`, which sets `msg_sender` to a null address. The called function must use `maybe_msg_sender()` to handle this. - Passing arguments to a public function from a private function. All of those arguments will be publicly visible. - Calling an internal public function from a private function. The fact that the call originated from a private function of that same contract will be trivially known. - Emitting unencrypted events from a private function. The unencrypted event name and arguments will be publicly visible. @@ -81,17 +81,13 @@ A 'Function Fingerprint' is any data which is exposed by a function to the outsi - The contents of L2 -> L1 messages. - All public logs (topics and arguments). - The roots of all trees which have been read from. -- The _number_ of ['side effects'](https://en.wikipedia.org/wiki/Side_effect_(computer_science)): - - \# new note hashes - - \# new nullifiers - - \# bytes of encrypted logs +- The _number_ of some ['side effects'](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). Note hashes, nullifiers, and private logs are padded to hide their true counts, but the following remain visible: - \# public function calls - \# L2->L1 messages - - \# nonzero roots > Note: many of these were mentioned in the ["Crossing the private to public boundary"](#crossing-the-private-to-public-boundary) section. -> Note: the transaction effects submitted to L1 are [encoded (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/Decoder.sol) but not garbled with other transactions. The distinct Tx Fingerprint of each transaction is publicly visible when submitted to the L2 tx pool. +> Note: a transaction's Tx Fingerprint is the combined set of publicly observable data listed above (for example: the number of public function calls, the number of L2->L1 messages, the contents of public logs, and which tree roots were read). Anyone watching the L2 transaction pool can see this fingerprint for every transaction that is submitted, and transactions with distinctive fingerprints can be linked to specific contracts or to patterns of user behavior. #### Standardizing Fingerprints diff --git a/docs/docs-participate/alpha.md b/docs/docs-participate/alpha.md new file mode 100644 index 000000000000..665e2c8c76ce --- /dev/null +++ b/docs/docs-participate/alpha.md @@ -0,0 +1,129 @@ +--- +title: Alpha Network +description: "Understand the Aztec Alpha network: its purpose, known limitations, expected issues, and what to expect as the protocol matures." +displayed_sidebar: participateSidebar +--- + +Alpha is the Aztec mainnet in its initial operational phase. It is live on Ethereum mainnet with real staking, governance, and user transactions. It is also early, unaudited software where bugs, including critical ones, are expected. + +This page is the central reference for understanding what Alpha is, what risks come with using it, and how the network will evolve. + +## What Alpha is + +Alpha is the first production deployment of the Aztec rollup. Governance, staking, and block production are fully operational with real economic stakes. Sequencers must stake real tokens to participate, and governance proposals have real consequences. + +However, "production" does not mean "finished." Alpha exists to: + +- Battle-test the protocol under real conditions +- Establish decentralized governance and validator sets +- Identify bugs that only surface at scale or under adversarial conditions +- Build toward a mature, stable mainnet + +Think of Alpha as a live stress test with real stakes. The protocol is functional, but it is not yet hardened. + +## What to build on Alpha + +Alpha is an experimentation phase for developers as much as it is for the protocol. This is the moment to try things that were not possible before: private smart contracts, hybrid public/private applications, novel privacy-preserving primitives, and patterns that no other chain can support. + +Much like the early days of Ethereum, the applications built now will shape what Aztec becomes. Expect rough edges, breaking changes, and the need to redeploy after upgrades. In exchange, you get a first-mover opportunity to explore a new design space alongside the protocol itself. Build on [Testnet](/networks#testnet) first to iterate quickly, then deploy to Alpha when you are ready to validate against real network conditions. + +## Known limitations and expected issues + +See the [Limitations](/developers/docs/resources/considerations/limitations) page for the full list of current developer-facing limitations. The sections below summarize the highest-impact issues for Alpha users. + +### Proving system bugs + +The Aztec proving system is novel and complex. Bugs in proof generation, verification, and circuit constraints are expected during Alpha. Some circuits are still under-constrained, meaning that soundness is not fully guaranteed. In practice, this means: + +- Provers may occasionally produce invalid proofs +- Proof verification on L1 may encounter edge cases +- Block production may halt temporarily while issues are diagnosed and patched + +These are known risks of an early-stage ZK rollup, not unexpected failures. + +### Unaudited software + +No part of the Aztec stack has been fully audited. The protocol, smart contracts, client software, and cryptographic primitives are all under active development. Code is being iterated on daily and audits are ongoing. Published audit reports are available in the [Aztec audit reports repository](https://github.com/AztecProtocol/audit-reports). + +### Privacy is not guaranteed + +Some privacy features are still in development. Known leakage includes: + +- The number of side effects in a transaction is visible (private transactions can be fingerprinted based on their side effect count) +- No privacy-preserving queries to third-party nodes exist yet +- New privacy standards for smart contract design have not been established + +### Circuit and transaction limits + +ZK-SNARK circuits impose hard upper bounds on what a single transaction can do, including the number of state reads and writes, notes, nullifiers, logs, and messages. Deeply nested function calls can exceed per-transaction limits. See [Limitations](/developers/docs/resources/considerations/limitations#circuit-limitations) for current constants. + +## State migration and rollup upgrades + +As the protocol matures, it will undergo rollup upgrades through the [governance process](/participate/governance/upgrades). When a new rollup version is deployed: + +- A new rollup contract is added to the onchain Registry +- The new rollup becomes canonical (receives block rewards) +- The old rollup remains accessible for bridging assets in and out + +### State migration + +Migrating application state from one rollup version to another is a hard problem for any ZK rollup, and Aztec is no exception. You should expect that: + +- **State does not carry over automatically.** Application data, deployed contracts, and notes on the current rollup will not be directly accessible on a new rollup version without explicit migration steps. +- **Migration tooling is still being developed.** There are no mature, battle-tested tools for migrating L2 state across rollup upgrades yet. +- **Applications will need to redeploy.** Developers should plan that contracts will need to be redeployed and state reconstructed (or omitted) after a rollup upgrade. +- **User funds are not permanently locked, but they are not immune to protocol bugs.** The Registry model ensures that users can always bridge assets out of any historical rollup version. However, a proving system bug or other protocol flaw could still allow funds to be stolen before guardrails are in place, so "not locked" is not the same as "risk-free." + +If you are building on Alpha, design your application with rollup upgrades in mind. Avoid assumptions about state permanence at this stage. + +For details on how rollup upgrades work, see [Network Upgrades](/participate/governance/upgrades). + +## Security disclosures + +We expect bugs, including critical ones, to be discovered during Alpha. Aztec is preparing a bug bounty program and actively conducts internal and external reviews. + +### Reporting vulnerabilities + +If you discover a security vulnerability: + +1. **Do not** open a public GitHub issue or pull request +2. Use [GitHub Private Vulnerability Reporting](https://github.com/AztecProtocol/aztec-packages/security/advisories/new) to submit details +3. You can also email [security@aztec.foundation](mailto:security@aztec.foundation) (but submit full details through GitHub, not email) + +Mark reports as **CRITICAL** if you believe a vulnerability is actively being exploited or could result in loss of funds, key compromise, or broad user impact. + +For the full security policy, see [SECURITY.md](https://github.com/AztecProtocol/aztec-packages/blob/master/SECURITY.md). + +### Non-security bugs + +For bugs that are not security-sensitive (performance issues, feature requests, unexpected behavior), open a [GitHub Issue](https://github.com/AztecProtocol/aztec-packages/issues). Keeping non-security bugs public helps the community track progress and collaborate on fixes. + +## Path to beta + +The transition from Alpha to Beta is defined by performance milestones, not a specific calendar date. The network will be considered Beta when it consistently meets the following targets: + +| Metric | Target | +| -------------------------- | ---------- | +| **User-perceived latency** | 12s median | +| **Sustained throughput** | 10 TPS | +| **Uptime** | 99.9% | + +These thresholds reflect the minimum bar for a network that application developers and users can rely on for production workloads. Until they are met, expect the instability and limitations described on this page. + +## What this means for you + +| If you are a... | Expect... | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Validator / Sequencer** | Occasional downtime, software updates, and proving failures. Stay current with node releases. | +| **Developer** | Breaking changes, API instability, and the need to redeploy after upgrades. Build on [Testnet](/networks#testnet) first, and design for impermanence. | +| **Governance Participant** | An active role in shaping the protocol. Upgrades are real and consequential, so participate in proposals and voting. | +| **User** | A functional but rough experience. Do not store meaningful secrets or rely on state permanence. | + +## Next steps + +- [Networks Overview](/networks): Technical details, RPC endpoints, and contract addresses +- [Limitations](/developers/docs/resources/considerations/limitations): Full list of current developer-facing limitations +- [Privacy Considerations](/developers/docs/resources/considerations/privacy_considerations): What leaks and what doesn't +- [Network Upgrades](/participate/governance/upgrades): How rollup upgrades work through governance +- [Security Policy](https://github.com/AztecProtocol/aztec-packages/blob/master/SECURITY.md): How to report vulnerabilities +- [Operator Guides](/operate/operators): Running network infrastructure diff --git a/docs/docs-participate/index.md b/docs/docs-participate/index.md index 7e249d52489e..31a2b2a39f10 100644 --- a/docs/docs-participate/index.md +++ b/docs/docs-participate/index.md @@ -8,6 +8,10 @@ displayed_sidebar: participateSidebar Welcome to the Participate section. Here you'll find educational content about how the Aztec network operates, the $AZTEC token, and how governance works. +:::caution Alpha Network +Aztec is currently in its **Alpha** phase, a live mainnet where bugs, including critical ones, are expected. Before using the network, read the [Alpha Network](/participate/alpha) page to understand current limitations, security expectations, and what to expect from rollup upgrades. +::: + ## Basics of Aztec New to Aztec? Start here to understand the fundamentals: diff --git a/docs/docs/networks.md b/docs/docs/networks.md index 1ca5d3d6551e..f7ea79b575ec 100644 --- a/docs/docs/networks.md +++ b/docs/docs/networks.md @@ -90,7 +90,11 @@ Not sure which network to use? Jump to our [Network Selection Guide](#network-se ### Ignition (Mainnet - Phase 1) +<<<<<<< HEAD Ignition is the Aztec **mainnet** in its first operational phase, focusing on establishing governance and network infrastructure. +======= +Alpha is the Aztec **mainnet** in its initial operational phase, with governance, networking, and transaction processing fully active. Alpha is live but early — bugs, including critical ones, are expected. For a full explanation of what this means, see the **[Alpha Network](/participate/alpha)** page. +>>>>>>> 63c67a0f44 (docs: add Alpha Network page and update privacy/limitations docs (#22515)) #### Overview diff --git a/docs/sidebars-participate.js b/docs/sidebars-participate.js index 6915cb5f9c3a..7b7c2c8da6b9 100644 --- a/docs/sidebars-participate.js +++ b/docs/sidebars-participate.js @@ -6,6 +6,7 @@ const sidebars = { participateSidebar: [ { type: "doc", id: "index", label: "Overview" }, + { type: "doc", id: "alpha", label: "Alpha Network" }, { type: "html", value: 'Basics of Aztec', From 61cb283b77f29a5e4ce6e980d364e8bb45e93f16 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 21 Apr 2026 21:13:05 +0000 Subject: [PATCH 14/14] fix: resolve cherry-pick conflicts docs/CLAUDE.md: take newer timestamp/version from PR #22515 docs/docs/networks.md: keep v4-next Ignition wording for Phase 1, add pointer to new Alpha Network page --- docs/CLAUDE.md | 5 ----- docs/docs/networks.md | 6 +----- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 4170cbc60f2b..c39fe15835ef 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -382,10 +382,5 @@ Approved external documentation sources: - Suggest improvements even if they go beyond pure editing - When making changes to documentation processes or tooling, remember to check and update READMEs, project documentation (like this file), and code comments -<<<<<<< HEAD -Last updated: 2026-02-23 -Version: 1.5 -======= Last updated: 2026-04-21 Version: 1.7 ->>>>>>> 63c67a0f44 (docs: add Alpha Network page and update privacy/limitations docs (#22515)) diff --git a/docs/docs/networks.md b/docs/docs/networks.md index f7ea79b575ec..e148a2ea8b08 100644 --- a/docs/docs/networks.md +++ b/docs/docs/networks.md @@ -90,11 +90,7 @@ Not sure which network to use? Jump to our [Network Selection Guide](#network-se ### Ignition (Mainnet - Phase 1) -<<<<<<< HEAD -Ignition is the Aztec **mainnet** in its first operational phase, focusing on establishing governance and network infrastructure. -======= -Alpha is the Aztec **mainnet** in its initial operational phase, with governance, networking, and transaction processing fully active. Alpha is live but early — bugs, including critical ones, are expected. For a full explanation of what this means, see the **[Alpha Network](/participate/alpha)** page. ->>>>>>> 63c67a0f44 (docs: add Alpha Network page and update privacy/limitations docs (#22515)) +Ignition is the Aztec **mainnet** in its first operational phase, focusing on establishing governance and network infrastructure. Ignition is live but early — bugs, including critical ones, are expected. For a full explanation of what this means, see the **[Alpha Network](/participate/alpha)** page. #### Overview