diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 13b1941442d9..6ec2d09effe8 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -21,6 +21,7 @@ members = [ "contracts/app/token_bridge_contract", "contracts/app/token_contract", "contracts/app/uniswap_contract", + "contracts/app/simple_token_playground", "contracts/docs/docs_example_contract", "contracts/fees/fpc_contract", "contracts/fees/sponsored_fpc_contract", diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_playground/Nargo.toml b/noir-projects/noir-contracts/contracts/app/simple_token_playground/Nargo.toml new file mode 100644 index 000000000000..ad858a41f8e5 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/app/simple_token_playground/Nargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simple_token_playground" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } +uint_note = { path = "../../../../aztec-nr/uint-note" } +compressed_string = { path = "../../../../aztec-nr/compressed-string" } +authwit = { path = "../../../../aztec-nr/authwit" } diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/main.nr b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/main.nr new file mode 100644 index 000000000000..106a3158c86d --- /dev/null +++ b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/main.nr @@ -0,0 +1,367 @@ +mod types; + +use dep::aztec::macros::aztec; + +// Minimal token contract. Do not use +// For demonstration purposes in playground only +// If you change the names of these functions, please also update them in playground/src/components/contract/contract.ts + +#[aztec] +pub contract SimpleToken { + use std::{meta::derive, ops::{Add, Sub}}; + + use dep::compressed_string::FieldCompressedString; + + use dep::aztec::{ + context::{PrivateCallInterface, PrivateContext}, + encrypted_logs::log_assembly_strategies::default_aes128::{ + event::encode_and_encrypt_event_unconstrained, + note::{encode_and_encrypt_note, encode_and_encrypt_note_unconstrained}, + }, + event::event_interface::EventInterface, + macros::{ + events::event, + functions::{initializer, internal, private, public, utility, view}, + storage::storage, + }, + prelude::{AztecAddress, Map, PublicContext, PublicImmutable, PublicMutable}, + protocol_types::traits::Serialize, + }; + + use dep::uint_note::uint_note::{PartialUintNote, UintNote}; + use aztec::protocol_types::traits::ToField; + + use dep::authwit::auth::{ + assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, + compute_authwit_nullifier, + }; + + use crate::types::balance_set::BalanceSet; + + global INITIAL_TRANSFER_CALL_MAX_NOTES: u32 = 2; + global RECURSIVE_TRANSFER_CALL_MAX_NOTES: u32 = 8; + + #[derive(Serialize)] + #[event] + struct Transfer { + from: AztecAddress, + to: AztecAddress, + amount: u128, + } + + #[storage] + struct Storage { + balances: Map, Context>, + total_supply: PublicMutable, + public_balances: Map, Context>, + symbol: PublicImmutable, + name: PublicImmutable, + decimals: PublicImmutable, + } + + #[public] + #[initializer] + fn constructor(name: str<31>, symbol: str<31>, decimals: u8) { + storage.name.initialize(FieldCompressedString::from_string(name)); + storage.symbol.initialize(FieldCompressedString::from_string(symbol)); + storage.decimals.initialize(decimals); + } + + #[public] + #[view] + fn public_get_name() -> FieldCompressedString { + storage.name.read() + } + + #[public] + #[view] + fn public_get_symbol() -> pub FieldCompressedString { + storage.symbol.read() + } + + #[public] + #[view] + fn public_get_decimals() -> pub u8 { + storage.decimals.read() + } + + #[public] + #[view] + fn public_total_supply() -> u128 { + storage.total_supply.read() + } + + #[public] + #[view] + fn public_balance_of(owner: AztecAddress) -> u128 { + storage.public_balances.at(owner).read() + } + + #[utility] + pub(crate) unconstrained fn private_balance_of(owner: AztecAddress) -> u128 { + storage.balances.at(owner).balance_of() + } + + #[public] + fn mint_publicly(to: AztecAddress, amount: u128) { + let new_balance = storage.public_balances.at(to).read().add(amount); + let supply = storage.total_supply.read().add(amount); + storage.public_balances.at(to).write(new_balance); + storage.total_supply.write(supply); + } + + #[public] + fn public_transfer(from: AztecAddress, to: AztecAddress, amount: u128, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + let from_balance = storage.public_balances.at(from).read().sub(amount); + storage.public_balances.at(from).write(from_balance); + let to_balance = storage.public_balances.at(to).read().add(amount); + storage.public_balances.at(to).write(to_balance); + } + + #[public] + fn burn_public(from: AztecAddress, amount: u128, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit_public(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + let from_balance = storage.public_balances.at(from).read().sub(amount); + storage.public_balances.at(from).write(from_balance); + let new_supply = storage.total_supply.read().sub(amount); + storage.total_supply.write(new_supply); + } + + #[private] + fn transfer_from_private_to_public( + from: AztecAddress, + to: AztecAddress, + amount: u128, + nonce: Field, + ) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note( + &mut context, + from, + from, + )); + SimpleToken::at(context.this_address())._increase_public_balance(to, amount).enqueue( + &mut context, + ); + } + + #[private] + fn private_transfer(from: AztecAddress, to: AztecAddress, amount: u128, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + + storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note( + &mut context, + from, + from, + )); + storage.balances.at(to).add(to, amount).emit(encode_and_encrypt_note(&mut context, to, from)); + } + + #[private] + fn burn_private(from: AztecAddress, amount: u128, nonce: Field) { + if (!from.eq(context.msg_sender())) { + assert_current_call_valid_authwit(&mut context, from); + } else { + assert(nonce == 0, "invalid nonce"); + } + storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note( + &mut context, + from, + from, + )); + SimpleToken::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context); + } + + #[private] + fn transfer_from_public_to_private(to: AztecAddress, amount: u128) { + let from = context.msg_sender(); + let token = SimpleToken::at(context.this_address()); + + let partial_note = _prepare_private_balance_increase(from, to, &mut context, storage); + token._finalize_transfer_to_private_unsafe(from, amount, partial_note).enqueue(&mut context); + } + + #[private] + fn prepare_private_balance_increase(to: AztecAddress, from: AztecAddress) -> PartialUintNote { + _prepare_private_balance_increase(from, to, &mut context, storage) + } + + #[contract_library_method] + fn _prepare_private_balance_increase( + from: AztecAddress, + to: AztecAddress, + context: &mut PrivateContext, + storage: Storage<&mut PrivateContext>, + ) -> PartialUintNote { + let partial_note = UintNote::partial( + to, + storage.balances.at(to).set.storage_slot, + context, + to, + from, + ); + + SimpleToken::at(context.this_address()) + ._store_balances_set_partial_note(partial_note) + .enqueue(context); + + partial_note + } + + #[public] + fn finalize_transfer_to_private(amount: u128, partial_note: PartialUintNote) { + let from = context.msg_sender(); + _finalize_transfer_to_private(from, amount, partial_note, &mut context, storage); + } + + #[public] + #[internal] + fn _finalize_transfer_to_private_unsafe( + from: AztecAddress, + amount: u128, + partial_note: PartialUintNote, + ) { + _finalize_transfer_to_private(from, amount, partial_note, &mut context, storage); + } + + #[contract_library_method] + fn _finalize_transfer_to_private( + from: AztecAddress, + amount: u128, + partial_note: PartialUintNote, + context: &mut PublicContext, + storage: Storage<&mut PublicContext>, + ) { + let from_balance = storage.public_balances.at(from).read().sub(amount); + storage.public_balances.at(from).write(from_balance); + + assert(context.storage_read(partial_note.commitment()), "Invalid partial note"); + partial_note.complete(amount, context); + } + + #[private] + fn mint_privately(from: AztecAddress, to: AztecAddress, amount: u128) { + let token = SimpleToken::at(context.this_address()); + let partial_note = _prepare_private_balance_increase(from, to, &mut context, storage); + token._finalize_mint_to_private_unsafe(amount, partial_note).enqueue(&mut context); + } + + #[public] + fn finalize_mint_to_private(amount: u128, partial_note: PartialUintNote) { + _finalize_mint_to_private(amount, partial_note, &mut context, storage); + } + + #[public] + #[internal] + fn _finalize_mint_to_private_unsafe(amount: u128, partial_note: PartialUintNote) { + _finalize_mint_to_private(amount, partial_note, &mut context, storage); + } + + #[contract_library_method] + fn _finalize_mint_to_private( + amount: u128, + partial_note: PartialUintNote, + context: &mut PublicContext, + storage: Storage<&mut PublicContext>, + ) { + let supply = storage.total_supply.read().add(amount); + storage.total_supply.write(supply); + + assert(context.storage_read(partial_note.commitment()), "Invalid partial note"); + partial_note.complete(amount, context); + } + + #[public] + #[internal] + fn _store_balances_set_partial_note(partial_note: PartialUintNote) { + context.storage_write(partial_note.commitment(), true); + } + + #[public] + #[internal] + fn _increase_public_balance(to: AztecAddress, amount: u128) { + _increase_public_balance_inner(to, amount, storage); + } + + #[contract_library_method] + fn _increase_public_balance_inner( + to: AztecAddress, + amount: u128, + storage: Storage<&mut PublicContext>, + ) { + let new_balance = storage.public_balances.at(to).read().add(amount); + storage.public_balances.at(to).write(new_balance); + } + + #[public] + #[internal] + fn _reduce_total_supply(amount: u128) { + let new_supply = storage.total_supply.read().sub(amount); + storage.total_supply.write(new_supply); + } + + #[private] + fn cancel_authwit(inner_hash: Field) { + let on_behalf_of = context.msg_sender(); + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + context.push_nullifier(nullifier); + } + + #[contract_library_method] + fn subtract_balance( + context: &mut PrivateContext, + storage: Storage<&mut PrivateContext>, + account: AztecAddress, + amount: u128, + max_notes: u32, + ) -> u128 { + let subtracted = storage.balances.at(account).try_sub(amount, max_notes); + assert(subtracted > 0 as u128, "Balance too low"); + if subtracted >= amount { + subtracted - amount + } else { + let remaining = amount - subtracted; + compute_recurse_subtract_balance_call(*context, account, remaining).call(context) + } + } + + #[no_predicates] + #[contract_library_method] + fn compute_recurse_subtract_balance_call( + context: PrivateContext, + account: AztecAddress, + remaining: u128, + ) -> PrivateCallInterface<25, u128> { + SimpleToken::at(context.this_address())._recurse_subtract_balance(account, remaining) + } + + #[internal] + #[private] + fn _recurse_subtract_balance(account: AztecAddress, amount: u128) -> u128 { + subtract_balance( + &mut context, + storage, + account, + amount, + RECURSIVE_TRANSFER_CALL_MAX_NOTES, + ) + } +} diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types.nr b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types.nr new file mode 100644 index 000000000000..1bc5f644e4ff --- /dev/null +++ b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types.nr @@ -0,0 +1 @@ +pub(crate) mod balance_set; diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types/balance_set.nr b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types/balance_set.nr new file mode 100644 index 000000000000..5c15966e6236 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/app/simple_token_playground/src/types/balance_set.nr @@ -0,0 +1,122 @@ +use dep::aztec::{ + context::{PrivateContext, UtilityContext}, + note::{note_emission::OuterNoteEmission, retrieved_note::RetrievedNote}, + protocol_types::{address::AztecAddress, constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL}, +}; +use dep::aztec::prelude::{NoteGetterOptions, NoteViewerOptions, PrivateSet}; +use dep::uint_note::uint_note::UintNote; +use std::ops::Add; + +pub struct BalanceSet { + pub set: PrivateSet, +} + +impl BalanceSet { + pub fn new(context: Context, storage_slot: Field) -> Self { + assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); + Self { set: PrivateSet::new(context, storage_slot) } + } +} + +impl BalanceSet { + pub unconstrained fn balance_of(self: Self) -> u128 { + self.balance_of_with_offset(0) + } + + pub unconstrained fn balance_of_with_offset(self: Self, offset: u32) -> u128 { + let mut balance = 0 as u128; + // docs:start:view_notes + let mut options = NoteViewerOptions::new(); + let notes = self.set.view_notes(options.set_offset(offset)); + // docs:end:view_notes + for i in 0..options.limit { + if i < notes.len() { + balance = balance + notes.get_unchecked(i).get_value(); + } + } + if (notes.len() == options.limit) { + balance = balance + self.balance_of_with_offset(offset + options.limit); + } + + balance + } +} + +impl BalanceSet<&mut PrivateContext> { + pub fn add(self: Self, owner: AztecAddress, addend: u128) -> OuterNoteEmission { + if addend == 0 as u128 { + OuterNoteEmission::new(Option::none()) + } else { + // We fetch the nullifier public key hash from the registry / from our PXE + let mut addend_note = UintNote::new(addend, owner); + + // docs:start:insert + OuterNoteEmission::new(Option::some(self.set.insert(addend_note))) + // docs:end:insert + } + } + + pub fn sub(self: Self, owner: AztecAddress, amount: u128) -> OuterNoteEmission { + let subtracted = self.try_sub(amount, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL); + + // try_sub may have substracted more or less than amount. We must ensure that we subtracted at least as much as + // we needed, and then create a new note for the owner for the change (if any). + assert(subtracted >= amount, "Balance too low"); + self.add(owner, subtracted - amount) + } + + // Attempts to remove 'target_amount' from the owner's balance. try_sub returns how much was actually subtracted + // (i.e. the sum of the value of nullified notes), but this subtracted amount may be more or less than the target + // amount. + // This may seem odd, but is unfortunately unavoidable due to the number of notes available and their amounts being + // unknown. What try_sub does is a best-effort attempt to consume as few notes as possible that add up to more than + // `target_amount`. + // The `max_notes` parameter is used to fine-tune the number of constraints created by this function. The gate count + // scales relatively linearly with `max_notes`, but a lower `max_notes` parameter increases the likelihood of + // `try_sub` subtracting an amount smaller than `target_amount`. + pub fn try_sub(self: Self, target_amount: u128, max_notes: u32) -> u128 { + // We are using a preprocessor here (filter applied in an unconstrained context) instead of a filter because + // we do not need to prove correct execution of the preprocessor. + // Because the `min_sum` notes is not constrained, users could choose to e.g. not call it. However, all this + // might result in is simply higher DA costs due to more nullifiers being emitted. Since we don't care + // about proving optimal note usage, we can save these constraints and make the circuit smaller. + let options = NoteGetterOptions::with_preprocessor(preprocess_notes_min_sum, target_amount) + .set_limit(max_notes); + let notes = self.set.pop_notes(options); + + let mut subtracted = 0 as u128; + for i in 0..options.limit { + if i < notes.len() { + let note = notes.get_unchecked(i); + subtracted = subtracted + note.get_value(); + } + } + + subtracted + } +} + +// Computes the partial sum of the notes array, stopping once 'min_sum' is reached. This can be used to minimize the +// number of notes read that add to some value, e.g. when transferring some amount of tokens. +// The preprocessor (a filter applied in an unconstrained context) does not check if total sum is larger or equal to +// 'min_sum' - all it does is remove extra notes if it does reach that value. +// Note that proper usage of this preprocessor requires for notes to be sorted in descending order. +pub fn preprocess_notes_min_sum( + notes: [Option>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL], + min_sum: u128, +) -> [Option>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] { + let mut selected = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; + let mut sum = 0 as u128; + for i in 0..notes.len() { + // Because we process notes in retrieved order, notes need to be sorted in descending amount order for this + // filter to be useful. Consider a 'min_sum' of 4, and a set of notes with amounts [3, 2, 1, 1, 1, 1, 1]. If + // sorted in descending order, the filter will only choose the notes with values 3 and 2, but if sorted in + // ascending order it will choose 4 notes of value 1. + if notes[i].is_some() & sum < min_sum { + let retrieved_note = notes[i].unwrap_unchecked(); + selected[i] = Option::some(retrieved_note); + sum = sum.add(retrieved_note.note.get_value()); + } + } + selected +} diff --git a/playground/index.html b/playground/index.html index 7d5b3aca71c4..489136abb4f9 100644 --- a/playground/index.html +++ b/playground/index.html @@ -3,6 +3,7 @@ + Aztec Playground diff --git a/playground/package.json b/playground/package.json index 3c04319c3707..9056a5658864 100644 --- a/playground/package.json +++ b/playground/package.json @@ -18,13 +18,17 @@ "dependencies": { "@aztec/accounts": "link:../yarn-project/accounts", "@aztec/aztec.js": "link:../yarn-project/aztec.js", + "@aztec/constants": "link:../yarn-project/constants", "@aztec/foundation": "link:../yarn-project/foundation", "@aztec/kv-store": "link:../yarn-project/kv-store", + "@aztec/noir-contracts.js": "link:../yarn-project/noir-contracts.js", "@aztec/pxe": "link:../yarn-project/pxe", "@aztec/stdlib": "link:../yarn-project/stdlib", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@fontsource/inter": "^5.2.5", "@fontsource/roboto": "^5.1.1", + "@fontsource/space-grotesk": "^5.2.6", "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", "@mui/styles": "^6.3.1", diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 428272900c27..c5e8e9c65c95 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -1,7 +1,7 @@ import { Global } from '@emotion/react'; import { ThemeProvider } from '@mui/material/styles'; import { globalStyle, theme } from './common.styles'; -import Home from './components/home/home'; +import Home from './components/home/Home'; function App() { return ( diff --git a/playground/src/assets/aztec_logo.png b/playground/src/assets/aztec_logo.png new file mode 100644 index 000000000000..44e18266831a Binary files /dev/null and b/playground/src/assets/aztec_logo.png differ diff --git a/playground/src/assets/Aztec_logo.png b/playground/src/assets/aztec_small_logo.png similarity index 100% rename from playground/src/assets/Aztec_logo.png rename to playground/src/assets/aztec_small_logo.png diff --git a/playground/src/assets/aztec_symbol.png b/playground/src/assets/aztec_symbol.png new file mode 100644 index 000000000000..21b602eb0690 Binary files /dev/null and b/playground/src/assets/aztec_symbol.png differ diff --git a/playground/src/assets/loading_icon.gif b/playground/src/assets/loading_icon.gif new file mode 100644 index 000000000000..0bf149931ced Binary files /dev/null and b/playground/src/assets/loading_icon.gif differ diff --git a/playground/src/assets/welcome_icon.svg b/playground/src/assets/welcome_icon.svg new file mode 100644 index 000000000000..028b1d59cb6e --- /dev/null +++ b/playground/src/assets/welcome_icon.svg @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playground/src/aztecEnv.ts b/playground/src/aztecEnv.ts index 7896e55f89f1..d9b75ae4cb92 100644 --- a/playground/src/aztecEnv.ts +++ b/playground/src/aztecEnv.ts @@ -3,17 +3,18 @@ import { type AztecNode, AztecAddress, AccountWalletWithSecretKey, - Contract, type PXE, type Logger, createLogger, + type ContractArtifact, } from '@aztec/aztec.js'; import { createPXEService, type PXEServiceConfig, getPXEServiceConfig } from '@aztec/pxe/client/lazy'; import { createStore } from '@aztec/kv-store/indexeddb'; import { createContext } from 'react'; import { NetworkDB, WalletDB } from './utils/storage'; -import { type ContractFunctionInteractionTx } from './utils/txs'; +import { type UserTx } from './utils/txs'; +import type { Network } from './utils/networks'; const logLevel = ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace'] as const; type LogLevel = (typeof logLevel)[number]; @@ -87,17 +88,21 @@ export class WebLogger { export const AztecContext = createContext<{ pxe: PXE | null; - nodeURL: string; + connecting: boolean; + network: Network; node: AztecNode; wallet: AccountWalletWithSecretKey | null; isPXEInitialized: boolean; walletDB: WalletDB | null; currentContractAddress: AztecAddress; - currentContract: Contract; - currentTx: ContractFunctionInteractionTx; + currentTx: UserTx; logs: Log[]; logsOpen: boolean; drawerOpen: boolean; + showContractInterface: boolean; + currentContractArtifact: ContractArtifact; + setShowContractInterface: (showContractInterface: boolean) => void; + setConnecting: (connecting: boolean) => void; setDrawerOpen: (drawerOpen: boolean) => void; setLogsOpen: (logsOpen: boolean) => void; setLogs: (logs: Log[]) => void; @@ -106,34 +111,38 @@ export const AztecContext = createContext<{ setWallet: (wallet: AccountWalletWithSecretKey) => void; setAztecNode: (node: AztecNode) => void; setPXE: (pxe: PXE) => void; - setNodeURL: (nodeURL: string) => void; - setCurrentTx: (currentTx: ContractFunctionInteractionTx) => void; - setCurrentContract: (currentContract: Contract) => void; + setNetwork: (network: Network) => void; + setCurrentTx: (currentTx: UserTx) => void; + setCurrentContractArtifact: (currentContract: ContractArtifact) => void; setCurrentContractAddress: (currentContractAddress: AztecAddress) => void; }>({ pxe: null, - nodeURL: '', + connecting: false, + network: null, node: null, wallet: null, isPXEInitialized: false, walletDB: null, - currentContract: null, + currentContractArtifact: null, currentContractAddress: null, currentTx: null, logs: [], logsOpen: false, drawerOpen: false, + showContractInterface: false, + setShowContractInterface: () => {}, + setConnecting: () => {}, setDrawerOpen: () => {}, setLogsOpen: () => {}, setLogs: () => {}, setWalletDB: () => {}, setPXEInitialized: () => {}, setWallet: () => {}, - setNodeURL: () => {}, + setNetwork: () => {}, setPXE: () => {}, setAztecNode: () => {}, setCurrentTx: () => {}, - setCurrentContract: () => {}, + setCurrentContractArtifact: () => {}, setCurrentContractAddress: () => {}, }); @@ -171,7 +180,7 @@ export class AztecEnv { const pxe = await createPXEService(aztecNode, configWithContracts, { loggers: { - store: WebLogger.getInstance().createLogger('pxe:data:indexeddb'), + store: WebLogger.getInstance().createLogger('pxe:data:idb'), pxe: WebLogger.getInstance().createLogger('pxe:service'), prover: WebLogger.getInstance().createLogger('bb:wasm:lazy'), }, diff --git a/playground/src/common.styles.tsx b/playground/src/common.styles.tsx index fa61b8e38d50..cb8bec5d879c 100644 --- a/playground/src/common.styles.tsx +++ b/playground/src/common.styles.tsx @@ -2,6 +2,10 @@ import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; +import '@fontsource/space-grotesk/500.css'; +import '@fontsource/space-grotesk/700.css'; +import '@fontsource/inter/400.css'; +import '@fontsource/inter/600.css'; import { css } from '@mui/styled-engine'; @@ -11,12 +15,66 @@ const themeOptions: ThemeOptions & { cssVariables: boolean } = { palette: { mode: 'light', primary: { - main: '#646cff', + main: '#9894FF', + light: '#CDD1D5', }, secondary: { main: '#f50057', }, }, + typography: { + fontFamily: '"Space Grotesk", "Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontFamily: '"Space Grotesk", sans-serif', + fontWeight: 500, + }, + h2: { + fontFamily: '"Space Grotesk", sans-serif', + fontWeight: 500, + }, + h5: { + fontFamily: '"Space Grotesk", sans-serif', + fontWeight: 700, + }, + button: { + fontFamily: '"Inter", sans-serif', + fontWeight: 600, + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + borderRadius: '12px', + }, + contained: { + backgroundColor: '#CDD1D5', + color: '#000000', + '&.Mui-disabled': { + backgroundColor: '#CDD1D5', + color: 'rgba(0, 0, 0, 0.38)', + }, + '&:hover': { + backgroundColor: '#BCC0C4', + }, + }, + containedPrimary: { + backgroundColor: '#9894FF', + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#8985FF', + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + borderRadius: '10px', + }, + }, + }, + }, cssVariables: true, }; @@ -28,10 +86,12 @@ export const globalStyle = css({ display: 'flex', minWidth: '100vw', minHeight: '100vh', - background: 'linear-gradient(#f6fbfc, #d8d4e7)', + background: 'linear-gradient(180deg, #9894FF 0%, #CDD1D5 100%)', + fontFamily: '"Space Grotesk", "Roboto", "Helvetica", "Arial", sans-serif', }, '#root': { width: '100%', + height: '100vh', }, }); diff --git a/playground/src/components/common/copyToClipboardButton.tsx b/playground/src/components/common/CopyToClipboardButton.tsx similarity index 100% rename from playground/src/components/common/copyToClipboardButton.tsx rename to playground/src/components/common/CopyToClipboardButton.tsx diff --git a/playground/src/components/common/FeePaymentSelector.tsx b/playground/src/components/common/FeePaymentSelector.tsx new file mode 100644 index 000000000000..dccd56f2f907 --- /dev/null +++ b/playground/src/components/common/FeePaymentSelector.tsx @@ -0,0 +1,80 @@ +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import FormControl from '@mui/material/FormControl'; +import { CircularProgress, MenuItem } from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; +import Typography from '@mui/material/Typography'; +import { FeeJuicePaymentMethod, type FeePaymentMethod } from '@aztec/aztec.js'; +import { AztecContext } from '../../aztecEnv'; +import { progressIndicator, select } from '../../styles/common'; +import { INFO_TEXT } from '../../constants'; +import { InfoText } from './InfoText'; + +const FeePaymentMethods = ['sponsored_fpc', 'private_fpc', 'public_fpc', 'fee_juice', 'bridged_fee_juice'] as const; +type FeePaymentMethodType = (typeof FeePaymentMethods)[number]; + +interface FeePaymentSelectorProps { + setFeePaymentMethod: (method: FeePaymentMethod) => void; +} + +export function FeePaymentSelector({ setFeePaymentMethod }: FeePaymentSelectorProps) { + const { pxe, network, wallet } = useContext(AztecContext); + + const [isMethodChanging, setIsMethodChanging] = useState(false); + const [selectedMethod, setSelectedMethod] = useState( + network.hasSponsoredFPC ? 'sponsored_fpc' : 'fee_juice', + ); + + useEffect(() => { + handleMethodChange(selectedMethod); + }, []); + + const handleMethodChange = async (method: FeePaymentMethodType) => { + setIsMethodChanging(true); + setSelectedMethod(method); + switch (method) { + case 'sponsored_fpc': { + const { prepareForFeePayment } = await import('../../utils/sponsoredFPC'); + const feePaymentMethod = await prepareForFeePayment(pxe); + setFeePaymentMethod(feePaymentMethod); + break; + } + case 'fee_juice': { + const feePaymentMethod = new FeeJuicePaymentMethod(wallet.getAddress()); + setFeePaymentMethod(feePaymentMethod); + break; + } + default: { + throw new Error('Unimplemented fee payment method'); + } + } + setIsMethodChanging(false); + }; + + return ( +
+ + Fee Payment Methods + + + {isMethodChanging && ( +
+ + Loading fee payment method... + + +
+ )} + {INFO_TEXT.FEE_ABSTRACTION} +
+ ); +} diff --git a/playground/src/components/common/fnParameter.tsx b/playground/src/components/common/FnParameter.tsx similarity index 95% rename from playground/src/components/common/fnParameter.tsx rename to playground/src/components/common/FnParameter.tsx index 3b0927df4d06..b638a7dbe02b 100644 --- a/playground/src/components/common/fnParameter.tsx +++ b/playground/src/components/common/FnParameter.tsx @@ -55,7 +55,8 @@ export function FunctionParameter({ setLoading(true); const accountAliases = await walletDB.listAliases('accounts'); const contractAliases = await walletDB.listAliases('contracts'); - setAliasedAddresses(parseAliasedBuffersAsString([...accountAliases, ...contractAliases])); + const senderAliases = await walletDB.listAliases('senders'); + setAliasedAddresses(parseAliasedBuffersAsString([...accountAliases, ...contractAliases, ...senderAliases])); setLoading(false); }; if (walletDB) { @@ -81,7 +82,7 @@ export function FunctionParameter({ onOpen={handleOpen} loading={loading} fullWidth - sx={{ width: '226px' }} + sx={{ width: '100%', minWidth: '226px' }} css={css} renderInput={params => ( + + + {children} + + + ); +} diff --git a/playground/src/components/common/LoadingModal.tsx b/playground/src/components/common/LoadingModal.tsx new file mode 100644 index 000000000000..0d49410d2e9f --- /dev/null +++ b/playground/src/components/common/LoadingModal.tsx @@ -0,0 +1,225 @@ +import { useContext, useEffect, useState } from 'react'; +import { css } from '@emotion/react'; +import { AztecContext } from '../../aztecEnv'; +import Typography from '@mui/material/Typography'; +import loadingIcon from '../../assets/loading_icon.gif'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import Button from '@mui/material/Button'; +import { Dialog } from '@mui/material'; +import { TxStatus } from '@aztec/aztec.js'; + +const TX_ERRORS = [ + 'error', + TxStatus.APP_LOGIC_REVERTED, + TxStatus.TEARDOWN_REVERTED, + TxStatus.BOTH_REVERTED, + TxStatus.DROPPED, +]; + +// Close button styling +const closeButton = css({ + position: 'absolute', + top: '10px', + right: '10px', +}); + +// Content group styling +const contentGroup = css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '2rem', + textWrap: 'wrap', + minHeight: '550px', + width: '600px', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-word', + '@media (max-width: 1200px)': { + width: 'unset', + }, +}); + +// Text styling +const titleText = css({ + fontFamily: '"Space Grotesk", sans-serif', + fontStyle: 'normal', + fontWeight: 500, + fontSize: '24px', + overflowWrap: 'break-word', + display: 'flex', + alignItems: 'center', + textAlign: 'center', + justifyContent: 'center', + letterSpacing: '-0.011em', + color: 'rgba(0, 0, 0, 0.8)', +}); + +// Subtitle text styling +const subtitleText = css({ + fontFamily: '"Inter", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '16px', + textAlign: 'center', + color: 'rgba(0, 0, 0, 0.6)', +}); + +// Loading animation styling +const loadingAnimation = css({ + width: '100px', + height: '100px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + margin: '20px 0', +}); + +// Error message styling +const errorMessage = css({ + fontFamily: '"Inter", sans-serif', + fontStyle: 'normal', + fontWeight: 500, + maxHeight: '80vh', + fontSize: '16px', + textAlign: 'center', + color: '#FF7764', +}); + +// Button container styling +const buttonContainer = css({ + position: 'absolute', + bottom: '20px', + display: 'flex', + gap: '10px', +}); + +// Fun facts styling +const funFactText = css({ + fontFamily: '"Inter", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '16px', + height: '60px', + lineHeight: '150%', + textAlign: 'center', + color: 'rgba(0, 0, 0, 0.6)', +}); + +const logContainer = css({ + marginTop: '2rem', + display: 'flex', + flexDirection: 'column', + textAlign: 'center', + alignItems: 'center', + width: '350px', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-word', +}); + +const logTitle = css({ + fontFamily: '"Inter", sans-serif', + fontStyle: 'normal', + fontWeight: 'bold', + fontSize: '14px', + textAlign: 'center', + color: 'rgba(0, 0, 0, 0.6)', +}); + +const logText = css({ + fontFamily: '"Inter", sans-serif', + fontStyle: 'normal', + fontWeight: 200, + height: '60px', + padding: '0.5rem', + fontSize: '12px', + textAlign: 'center', + color: 'rgba(0, 0, 0, 0.8)', +}); + +const funFacts = [ + 'Aztec has a super cool account abstraction model which you are utilizing right now', + "You're generating a client-side proof directly in your browser, and it won't take forever!", + 'Aztec enables programmable privacy across the entire Ethereum ecosystem', + 'Aztec uses zero-knowledge proofs to enable private transactions', + 'The Aztec protocol was founded in 2017', + "We're almost there...", + 'Aztec Connect was the first private DeFi application', + 'Aztec invented PLONK which is really cool', + 'Aztec supports private, public, and hybrid smart contract execution', + 'Aztec enables privacy and full composability', +]; + +export function LoadingModal() { + const { currentTx, setCurrentTx, logs } = useContext(AztecContext); + const [currentFunFact, setCurrentFunFact] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentFunFact(prev => (prev + 1) % funFacts.length); + }, 4000); + + return () => clearInterval(interval); + }, []); + + const handleClose = async () => { + // Set error state to indicate deployment was cancelled + if (currentTx && !TX_ERRORS.includes(currentTx.status)) { + setCurrentTx({ + ...currentTx, + status: 'error' as const, + error: 'Transaction cancelled by user', + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + setCurrentTx(null); + }; + + const isError = TX_ERRORS.includes(currentTx?.status); + const isProving = currentTx?.status === 'proving'; + + return ( + + + + +
+ + {isError + ? 'Error' + : isProving + ? 'Generating proof for transaction...' + : 'Sending transaction to Aztec network...'} + + {isError ? ( + <> + {currentTx.error || 'An error occurred'} +
+ +
+ + ) : ( + <> + + {isProving + ? 'A client-side zero-knowledge proof is being generated in your browser. This may take 20-60 seconds.' + : 'Your transaction is being sent to the Aztec network. This may take a few seconds.'} + + Loading... + Did you know? {funFacts[currentFunFact]} +
+ Don't click away! This is what we're currently working on: + {logs?.[0]?.message} +
+ + )} +
+
+ ); +} diff --git a/playground/src/components/contract/Contract.tsx b/playground/src/components/contract/Contract.tsx new file mode 100644 index 000000000000..92bd7fd47896 --- /dev/null +++ b/playground/src/components/contract/Contract.tsx @@ -0,0 +1,229 @@ +import { css } from '@mui/styled-engine'; +import { useContext, useEffect, useState } from 'react'; +import { + Contract, + type ContractInstanceWithAddress, + getAllFunctionAbis, + type FunctionAbi, + FunctionType, + DeployMethod, + type DeployOptions, +} from '@aztec/aztec.js'; +import { AztecContext } from '../../aztecEnv'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { formatFrAsString } from '../../utils/conversion'; +import { CreateContractDialog } from './components/CreateContractDialog'; +import ClearIcon from '@mui/icons-material/Clear'; +import { CopyToClipboardButton } from '../common/CopyToClipboardButton'; +import { ContractUpload } from './components/ContractUpload'; +import { ContractFilter } from './components/ContractFilter'; +import { FunctionCard } from './components/FunctionCard'; +import { useTransaction } from '../../hooks/useTransaction'; + +const container = css({ + display: 'flex', + height: '100%', + width: '100%', + overflow: 'hidden', + justifyContent: 'center', + alignItems: 'center', +}); + +const contractFnContainer = css({ + display: 'block', + width: '100%', + overflowY: 'auto', + color: 'black', + height: '100%', +}); + +const headerContainer = css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexWrap: 'wrap', + margin: '0 0.5rem', + padding: '0.1rem', + overflow: 'hidden', + justifyContent: 'stretch', + marginBottom: '0.5rem', +}); + +const header = css({ + display: 'flex', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', +}); + +const contractActions = css({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +const loadingArtifactContainer = css({ + display: 'flex', + flexDirection: 'column', + textAlign: 'center', + alignItems: 'center', + justifyContent: 'center', + gap: '2rem', +}); + +const contractName = css({ + marginRight: '0.5rem', + '@media (max-width: 1200px)': { + fontSize: '1.5rem', + }, +}); + +const FORBIDDEN_FUNCTIONS = ['process_log', 'sync_notes', 'public_dispatch']; + +export function ContractComponent() { + const [currentContract, setCurrentContract] = useState(null); + const [functionAbis, setFunctionAbis] = useState([]); + + const [filters, setFilters] = useState({ + searchTerm: '', + private: true, + public: true, + utility: true, + }); + + const [isLoadingArtifact, setIsLoadingArtifact] = useState(false); + + const [openCreateContractDialog, setOpenCreateContractDialog] = useState(false); + + const { sendTx } = useTransaction(); + + const { + node, + wallet, + currentContractAddress, + currentContractArtifact, + setCurrentContractArtifact, + setCurrentContractAddress, + } = useContext(AztecContext); + + useEffect(() => { + const loadCurrentContract = async () => { + setIsLoadingArtifact(true); + setFunctionAbis(getAllFunctionAbis(currentContractArtifact)); + setFilters({ + searchTerm: '', + private: true, + public: true, + utility: true, + }); + if (currentContractAddress && currentContract?.address !== currentContractAddress) { + const { isContractPubliclyDeployed } = await wallet.getContractMetadata(currentContractAddress); + // Temporarily filter out undeployed contracts + if (isContractPubliclyDeployed) { + const contractInstance = await node.getContract(currentContractAddress); + await wallet.registerContract({ instance: contractInstance, artifact: currentContractArtifact }); + const contract = await Contract.at(currentContractAddress, currentContractArtifact, wallet); + setCurrentContract(contract); + } + } else { + setCurrentContractAddress(null); + setCurrentContract(null); + } + setIsLoadingArtifact(false); + }; + if (!!currentContractArtifact) { + loadCurrentContract(); + } + }, [currentContractArtifact, currentContractAddress]); + + const handleContractCreation = async ( + contract?: ContractInstanceWithAddress, + publiclyDeploy?: boolean, + interaction?: DeployMethod, + opts?: DeployOptions, + ) => { + setOpenCreateContractDialog(false); + if (contract && publiclyDeploy) { + const deploymentResult = await sendTx( + `deploy ${currentContractArtifact.name}`, + interaction, + contract.address, + opts, + ); + // Temporarily ignore undeployed contracts + if (deploymentResult) { + setCurrentContractAddress(contract.address); + } + } + }; + + return ( +
+ {!currentContractArtifact ? ( + !isLoadingArtifact ? ( + + ) : ( +
+ Loading artifact... + +
+ ) + ) : ( +
+
+
+ + {currentContractArtifact.name} + + {!currentContractAddress && wallet && ( +
+ + +
+ )} + {currentContractAddress && ( +
+ {formatFrAsString(currentContractAddress.toString())} + + { + setCurrentContractAddress(null); + setCurrentContract(null); + setCurrentContractArtifact(null); + }} + > + + +
+ )} +
+ +
+ {functionAbis + .filter( + fn => + !fn.isInternal && + !FORBIDDEN_FUNCTIONS.includes(fn.name) && + ((filters.private && fn.functionType === FunctionType.PRIVATE) || + (filters.public && fn.functionType === FunctionType.PUBLIC) || + (filters.utility && fn.functionType === FunctionType.UTILITY)) && + (filters.searchTerm === '' || fn.name.includes(filters.searchTerm)), + ) + .map(fn => ( + + ))} +
+ )} +
+ ); +} diff --git a/playground/src/components/contract/components/ContractFilter.tsx b/playground/src/components/contract/components/ContractFilter.tsx new file mode 100644 index 000000000000..968601dc9275 --- /dev/null +++ b/playground/src/components/contract/components/ContractFilter.tsx @@ -0,0 +1,149 @@ +import { css } from '@mui/styled-engine'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import FindInPageIcon from '@mui/icons-material/FindInPage'; +import Input from '@mui/material/Input'; +import InputAdornment from '@mui/material/InputAdornment'; +import FormGroup from '@mui/material/FormGroup'; +import Tooltip from '@mui/material/Tooltip'; + +const search = css({ + display: 'flex', + overflow: 'hidden', + '@media (width <= 800px)': { + width: '100%', + }, + '@media (width > 800px)': { + maxWidth: '500px', + }, +}); + +const checkBoxLabel = css({ + height: '1.5rem', + marginLeft: '-10px', +}); + +interface ContractFilterProps { + filters: { + searchTerm: string; + private: boolean; + public: boolean; + utility: boolean; + }; + onFilterChange: (filters: { searchTerm: string; private: boolean; public: boolean; utility: boolean }) => void; +} + +export function ContractFilter({ filters, onFilterChange }: ContractFilterProps) { + return ( +
+ + onFilterChange({ ...filters, searchTerm: e.target.value })} + endAdornment={ + + + + } + /> +
+ + onFilterChange({ ...filters, private: e.target.checked })} + /> + } + label="Private" + /> + + + + onFilterChange({ ...filters, public: e.target.checked })} + /> + } + label="Public" + /> + + + + + onFilterChange({ + ...filters, + utility: e.target.checked, + }) + } + /> + } + label="Utility" + /> + +
+
+
+ ); +} diff --git a/playground/src/components/contract/components/ContractUpload.tsx b/playground/src/components/contract/components/ContractUpload.tsx new file mode 100644 index 000000000000..c77526885d35 --- /dev/null +++ b/playground/src/components/contract/components/ContractUpload.tsx @@ -0,0 +1,70 @@ +import { css } from '@mui/styled-engine'; +import { useDropzone } from 'react-dropzone'; +import './dropzone.css'; +import { loadContractArtifact } from '@aztec/aztec.js'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import { useContext } from 'react'; +import { AztecContext } from '../../../aztecEnv'; + +const dropZoneContainer = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + border: '3px dashed #9894FF', + borderRadius: '15px', + margin: '2rem 0', + backgroundColor: 'rgba(152, 148, 255, 0.04)', + alignItems: 'center', + justifyContent: 'center', +}); + +const uploadIcon = css({ + fontSize: '64px', + color: '#9894FF', + marginBottom: '1rem', +}); + +export function ContractUpload() { + const { setCurrentContractArtifact } = useContext(AztecContext); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop: async files => { + const file = files[0]; + const reader = new FileReader(); + reader.onload = async e => { + const contractArtifact = loadContractArtifact(JSON.parse(e.target?.result as string)); + setCurrentContractArtifact(contractArtifact); + }; + reader.readAsText(file); + }, + accept: { + 'application/json': ['.json'], + }, + multiple: false, + }); + + return ( +
+
+ + + + Upload Contract JSON Artifact + + Drag and drop a contract JSON file here, or click to select a file + + The contract artifact should be a JSON file exported from your Noir/Aztec project + + +
+
+ ); +} diff --git a/playground/src/components/contract/components/CreateAuthwitDialog.tsx b/playground/src/components/contract/components/CreateAuthwitDialog.tsx new file mode 100644 index 000000000000..37fbafb10ea5 --- /dev/null +++ b/playground/src/components/contract/components/CreateAuthwitDialog.tsx @@ -0,0 +1,173 @@ +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import { + type AbiType, + AztecAddress, + Contract, + ContractFunctionInteraction, + type SendMethodOptions, +} from '@aztec/aztec.js'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import FormControl from '@mui/material/FormControl'; +import FormGroup from '@mui/material/FormGroup'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { useContext, useState } from 'react'; +import { AztecContext } from '../../../aztecEnv'; +import { FunctionParameter } from '../../common/FnParameter'; +import { dialogBody, form, progressIndicator } from '../../../styles/common'; +import { INFO_TEXT } from '../../../constants'; +import { InfoText } from '../../common/InfoText'; +import { FeePaymentSelector } from '../../common/FeePaymentSelector'; +import { formatFrAsString } from '../../../utils/conversion'; +import { css } from '@emotion/react'; + +const aztecAddressTypeLike: AbiType = { + kind: 'struct', + path: 'address::AztecAddress', + fields: [{ name: 'inner', type: { kind: 'field' } }], +}; + +const fixedText = css({ + fontSize: '0.8rem', + fontStyle: 'italic', + color: 'rgba(0, 0, 0, 0.75)', + fontWeight: 200, + margin: 0, + padding: 0, +}); + +const authwitData = css({ + fontSize: '1rem', + fontWeight: 'bold', + margin: 0, +}); + +const authwitContainer = css({ + display: 'flex', + flexDirection: 'column', +}); + +interface CreateAuthwitDialogProps { + open: boolean; + contract: Contract; + fnName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; + isPrivate: boolean; + onClose: (isPublic?: boolean, interaction?: ContractFunctionInteraction, opts?: SendMethodOptions) => void; +} + +export function CreateAuthwitDialog({ open, contract, fnName, args, isPrivate, onClose }: CreateAuthwitDialogProps) { + const [alias, setAlias] = useState(''); + const [caller, setCaller] = useState(''); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + const [feePaymentMethod, setFeePaymentMethod] = useState(null); + + const { wallet, walletDB } = useContext(AztecContext); + + const handleClose = () => { + onClose(); + }; + + const createAuthwit = async () => { + setCreating(true); + try { + const action = contract.methods[fnName](...args); + let witness; + if (isPrivate) { + witness = await wallet.createAuthWit({ + caller: AztecAddress.fromString(caller), + action, + }); + await walletDB.storeAuthwitness(witness, undefined, alias); + onClose(); + } else { + const validateActionInteraction = await wallet.setPublicAuthWit( + { caller: AztecAddress.fromString(caller), action }, + true, + ); + const opts: SendMethodOptions = { fee: { paymentMethod: feePaymentMethod } }; + onClose(true, validateActionInteraction, opts); + } + } catch (e) { + setError(e.message); + } finally { + setAlias(''); + setCreating(false); + } + }; + + return ( + + Create authwit +
+ + +
+ Allow {' '} + {caller !== '' ? formatFrAsString(caller) : ''} + to call + + {fnName}({args.join(',')}) + + on contract + + {contract.artifact.name}@{formatFrAsString(contract.address.toString())} + +
+ {INFO_TEXT.AUTHWITS} + {isPrivate ? ( + + { + setAlias(event.target.value); + }} + /> + {INFO_TEXT.ALIASES} + + ) : ( + + )} +
+
+ {!error ? ( + creating ? ( +
+ + Creating authwitness... + + +
+ ) : ( + + ) + ) : ( + + An error occurred: {error} + + )} + +
+
+ ); +} diff --git a/playground/src/components/contract/components/CreateContractDialog.tsx b/playground/src/components/contract/components/CreateContractDialog.tsx new file mode 100644 index 000000000000..95a4368cf241 --- /dev/null +++ b/playground/src/components/contract/components/CreateContractDialog.tsx @@ -0,0 +1,203 @@ +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import { + type ContractInstanceWithAddress, + PublicKeys, + DeployMethod, + getContractInstanceFromDeployParams, + Contract, + type DeployOptions, + AztecAddress, + type Wallet, + Fr, +} from '@aztec/aztec.js'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import FormControl from '@mui/material/FormControl'; +import FormGroup from '@mui/material/FormGroup'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import { useContext, useEffect, useState } from 'react'; +import { + type ContractArtifact, + type FunctionAbi, + getDefaultInitializer, + getInitializer, + getAllFunctionAbis, +} from '@aztec/stdlib/abi'; +import { AztecContext } from '../../../aztecEnv'; +import { FunctionParameter } from '../../common/FnParameter'; +import { FeePaymentSelector } from '../../common/FeePaymentSelector'; +import { dialogBody, form, progressIndicator } from '../../../styles/common'; +import { InfoText } from '../../common/InfoText'; +import { INFO_TEXT } from '../../../constants'; + +export function CreateContractDialog({ + open, + contractArtifact, + onClose, +}: { + open: boolean; + contractArtifact: ContractArtifact; + onClose: ( + contract?: ContractInstanceWithAddress, + publiclyDeploy?: boolean, + interaction?: DeployMethod, + opts?: DeployOptions, + ) => void; +}) { + const [alias, setAlias] = useState(''); + const [initializer, setInitializer] = useState(null); + const [parameters, setParameters] = useState([]); + const { wallet, walletDB, pxe } = useContext(AztecContext); + const [functionAbis, setFunctionAbis] = useState([]); + + const [feePaymentMethod, setFeePaymentMethod] = useState(null); + const [publiclyDeploy, setPubliclyDeploy] = useState(true); + const [isRegistering, setIsRegistering] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const defaultInitializer = getDefaultInitializer(contractArtifact); + setInitializer(defaultInitializer); + setFunctionAbis(getAllFunctionAbis(contractArtifact)); + }, [contractArtifact]); + + const handleParameterChange = (index, value) => { + parameters[index] = value; + setParameters(parameters); + }; + + const handleClose = () => { + onClose(); + }; + + const createContract = async () => { + setIsRegistering(true); + try { + const salt = Fr.random(); + const contract = await getContractInstanceFromDeployParams(contractArtifact, { + publicKeys: PublicKeys.default(), + constructorArtifact: initializer, + constructorArgs: parameters, + deployer: wallet.getAddress(), + salt, + }); + await pxe.registerContract({ instance: contract, artifact: contractArtifact }); + await walletDB.storeContract(contract.address, contractArtifact, undefined, alias); + let deployMethod: DeployMethod; + let opts: DeployOptions; + if (publiclyDeploy) { + const postDeployCtor = (address: AztecAddress, wallet: Wallet) => + Contract.at(address, contractArtifact, wallet); + deployMethod = new DeployMethod( + contract.publicKeys, + wallet, + contractArtifact, + postDeployCtor, + parameters, + initializer.name, + ); + opts = { + contractAddressSalt: salt, + fee: { paymentMethod: feePaymentMethod }, + }; + onClose(contract, publiclyDeploy, deployMethod, opts); + } + } catch (e) { + setError(e.message); + } finally { + setIsRegistering(false); + } + }; + + return ( + + Create contract +
+ + + Initializer + + {initializer && + initializer.parameters.map((param, i) => ( + { + handleParameterChange(i, newValue); + }} + /> + ))} + + + { + setAlias(event.target.value); + }} + /> + {INFO_TEXT.ALIASES} + + {/* Always deploy for now */} + {/* + setPubliclyDeploy(event.target.checked)} /> + } + label="Deploy" + /> + */} + {publiclyDeploy && } + +
+ {!error ? ( + isRegistering ? ( +
+ + Registering contract... + + +
+ ) : ( + + ) + ) : ( + + An error occurred: {error} + + )} + +
+
+ ); +} diff --git a/playground/src/components/contract/components/FunctionCard.tsx b/playground/src/components/contract/components/FunctionCard.tsx new file mode 100644 index 000000000000..fb8b471b0414 --- /dev/null +++ b/playground/src/components/contract/components/FunctionCard.tsx @@ -0,0 +1,225 @@ +import { css } from '@mui/styled-engine'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import { + FunctionType, + type FunctionAbi, + ContractFunctionInteraction, + Contract, + type SendMethodOptions, + AuthWitness, + AztecAddress, +} from '@aztec/aztec.js'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import PsychologyIcon from '@mui/icons-material/Psychology'; +import SendIcon from '@mui/icons-material/Send'; +import VpnKeyIcon from '@mui/icons-material/VpnKey'; + +import FormGroup from '@mui/material/FormGroup'; +import { FunctionParameter } from '../../common/FnParameter'; +import { useContext, useState } from 'react'; +import { AztecContext } from '../../../aztecEnv'; +import { SendTxDialog } from './SendTxDialog'; +import { CreateAuthwitDialog } from './CreateAuthwitDialog'; + +type SimulationResult = { + success: boolean; + data?: any; + error?: string; +}; + +const simulationContainer = css({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + textOverflow: 'ellipsis', +}); + +const functionName = css({ + marginBottom: '1rem', + '@media (max-width: 1200px)': { + fontSize: '1.2rem', + }, +}); + +interface FunctionCardProps { + fn: FunctionAbi; + contract?: Contract; + onSendTxRequested: ( + name?: string, + interaction?: ContractFunctionInteraction, + contractAddress?: AztecAddress, + opts?: SendMethodOptions, + ) => void; +} + +export function FunctionCard({ fn, contract, onSendTxRequested }: FunctionCardProps) { + const [isWorking, setIsWorking] = useState(false); + const [parameters, setParameters] = useState([]); + const [simulationResults, setSimulationResults] = useState(); + + const [openSendTxDialog, setOpenSendTxDialog] = useState(false); + const [openCreateAuthwitDialog, setOpenCreateAuthwitDialog] = useState(false); + + const { wallet } = useContext(AztecContext); + + const simulate = async (fnName: string) => { + setIsWorking(true); + let result; + try { + const call = contract.methods[fnName](...parameters); + + result = await call.simulate({ skipFeeEnforcement: true }); + setSimulationResults({ success: true, data: result }); + } catch (e) { + setSimulationResults({ success: false, error: e.message }); + } + + setIsWorking(false); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleParameterChange = (index: number, value: any) => { + parameters[index] = value; + setParameters([...parameters]); + }; + + const handleAuthwitCreation = async ( + isPublic?: boolean, + interaction?: ContractFunctionInteraction, + opts?: SendMethodOptions, + ) => { + setOpenCreateAuthwitDialog(false); + if (isPublic && interaction && opts) { + onSendTxRequested(`${fn.name} public authwit`, interaction, contract?.address, opts); + } + }; + + const handleSendDialogClose = async ( + name?: string, + interaction?: ContractFunctionInteraction, + opts?: SendMethodOptions, + ) => { + setOpenSendTxDialog(false); + if (name && interaction && opts) { + onSendTxRequested(name, interaction, contract.address, opts); + } + }; + + return ( + + + + {fn.functionType} + + + {fn.name} + + {fn.parameters.length > 0 && ( + <> + + Parameters + + + {fn.parameters.map((param, i) => ( + { + handleParameterChange(i, newValue); + }} + /> + ))} + + + )} + + {!isWorking && simulationResults !== undefined && ( +
+ + Simulation results:  + + {simulationResults?.success ? ( + + {simulationResults?.data.length === 0 ? '-' : simulationResults?.data.toString()} + + ) : ( + + {simulationResults?.error} + + )}{' '} +
+ )} + {isWorking ? : <>} +
+ + + + + + {contract && openSendTxDialog && ( + + )} + {contract && openCreateAuthwitDialog && ( + + )} +
+ ); +} diff --git a/playground/src/components/contract/components/SendTxDialog.tsx b/playground/src/components/contract/components/SendTxDialog.tsx new file mode 100644 index 000000000000..3d49fc7f579a --- /dev/null +++ b/playground/src/components/contract/components/SendTxDialog.tsx @@ -0,0 +1,119 @@ +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import { AuthWitness, ContractFunctionInteraction, type SendMethodOptions } from '@aztec/aztec.js'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import { css } from '@mui/styled-engine'; +import { useContext, useEffect, useState } from 'react'; +import { AztecContext } from '../../../aztecEnv'; +import FormControl from '@mui/material/FormControl'; +import { FeePaymentSelector } from '../../common/FeePaymentSelector'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import Autocomplete from '@mui/material/Autocomplete'; +import { parseAliasedBuffersAsString } from '../../../utils/conversion'; +import { progressIndicator } from '../../../styles/common'; +import Divider from '@mui/material/Divider'; + +const dialogBody = css({ + display: 'flex', + flexDirection: 'column', + padding: '1rem', + alignItems: 'center', + minWidth: '350px', + minHeight: '500px', +}); + +const form = css({ + width: '100%', + display: 'flex', + gap: '1rem', +}); + +interface SendTxDialogProps { + name: string; + interaction: ContractFunctionInteraction; + open: boolean; + onClose: (name?: string, tx?: ContractFunctionInteraction, opts?: SendMethodOptions) => void; +} + +export function SendTxDialog({ name, interaction, open, onClose }: SendTxDialogProps) { + const [feePaymentMethod, setFeePaymentMethod] = useState(null); + const [loading, setLoading] = useState(false); + + const [authWits, setAuthwits] = useState([]); + const [selectedAuthwits, setSelectedAuthwits] = useState([]); + + const { walletDB } = useContext(AztecContext); + + useEffect(() => { + const refreshAuthwits = async () => { + setLoading(true); + const authwitBuffers = await walletDB.listAliases('authwits'); + const authwits = parseAliasedBuffersAsString(authwitBuffers).map(({ key, value }) => ({ + key, + value: AuthWitness.fromString(value), + })); + setAuthwits(authwits); + setLoading(false); + }; + refreshAuthwits(); + }, []); + + const send = async () => { + onClose(name, interaction, { authWitnesses: selectedAuthwits, fee: { paymentMethod: feePaymentMethod } }); + }; + + const handleClose = () => { + onClose(); + }; + + return ( + + Send transaction +
+ + + + + {loading ? ( +
+ + Loading authwits... + + +
+ ) : ( + <> + + Include autwitnesses + + ({ + id: alias.key, + label: alias.key, + value: alias.value, + }))} + onChange={(_event, authwits) => setSelectedAuthwits(authwits.map(authwit => authwit.value))} + renderInput={params => ( + + )} + /> + + )} +
+ + +
+
+ ); +} diff --git a/playground/src/components/contract/components/createAuthwitDialog.tsx b/playground/src/components/contract/components/createAuthwitDialog.tsx deleted file mode 100644 index 70da45adca99..000000000000 --- a/playground/src/components/contract/components/createAuthwitDialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import DialogTitle from '@mui/material/DialogTitle'; -import Dialog from '@mui/material/Dialog'; -import { type AbiType, AuthWitness, AztecAddress } from '@aztec/aztec.js'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import FormControl from '@mui/material/FormControl'; -import FormGroup from '@mui/material/FormGroup'; -import TextField from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import { css } from '@mui/styled-engine'; -import { useContext, useState } from 'react'; -import { AztecContext } from '../../../aztecEnv'; -import { FunctionParameter } from '../../common/fnParameter'; - -const creationForm = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - padding: '1rem', - alignItems: 'center', -}); - -const aztecAddressTypeLike: AbiType = { - kind: 'struct', - path: 'address::AztecAddress', - fields: [{ name: 'inner', type: { kind: 'field' } }], -}; - -export function CreateAuthwitDialog({ - open, - fnName, - args, - isPrivate, - onClose, -}: { - open: boolean; - fnName: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any[]; - isPrivate: boolean; - onClose: (witness?: AuthWitness, alias?: string) => void; -}) { - const [alias, setAlias] = useState(''); - const [caller, setCaller] = useState(''); - const [creating, setCreating] = useState(false); - - const { wallet, currentContract } = useContext(AztecContext); - - const handleClose = () => { - onClose(); - }; - - const createAuthwit = async () => { - setCreating(true); - const action = currentContract.methods[fnName](...args); - let witness; - if (isPrivate) { - witness = await wallet.createAuthWit({ - caller: AztecAddress.fromString(caller), - action, - }); - } else { - const validateActionInteraction = await wallet.setPublicAuthWit( - { caller: AztecAddress.fromString(caller), action }, - true, - ); - await validateActionInteraction.send().wait(); - } - setAlias(''); - setCreating(false); - onClose(witness, alias); - }; - - return ( - - Create authwit -
- {creating ? ( - <> - Creating... - - - ) : ( - <> - - - {isPrivate && ( - - { - setAlias(event.target.value); - }} - /> - - )} - - - - - )} -
-
- ); -} diff --git a/playground/src/components/contract/components/deployContractDialog.tsx b/playground/src/components/contract/components/deployContractDialog.tsx deleted file mode 100644 index 9e07940d8dc3..000000000000 --- a/playground/src/components/contract/components/deployContractDialog.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import DialogTitle from '@mui/material/DialogTitle'; -import Dialog from '@mui/material/Dialog'; -import { ContractDeployer, type ContractInstanceWithAddress, PublicKeys } from '@aztec/aztec.js'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import FormControl from '@mui/material/FormControl'; -import FormGroup from '@mui/material/FormGroup'; -import TextField from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import InputLabel from '@mui/material/InputLabel'; -import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; -import { css } from '@mui/styled-engine'; -import { useContext, useEffect, useState } from 'react'; -import { - type ContractArtifact, - encodeArguments, - type FunctionAbi, - getDefaultInitializer, - getInitializer, - getAllFunctionAbis, -} from '@aztec/stdlib/abi'; -import { AztecContext } from '../../../aztecEnv'; -import { FunctionParameter } from '../../common/fnParameter'; - -const creationForm = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - padding: '1rem', - alignItems: 'center', -}); - -export function DeployContractDialog({ - open, - contractArtifact, - onClose, -}: { - open: boolean; - contractArtifact: ContractArtifact; - onClose: (contract?: ContractInstanceWithAddress, alias?: string) => void; -}) { - const [alias, setAlias] = useState(''); - const [initializer, setInitializer] = useState(null); - const [parameters, setParameters] = useState([]); - const [deploying, setDeploying] = useState(false); - const { wallet, setLogsOpen } = useContext(AztecContext); - const [functionAbis, setFunctionAbis] = useState([]); - - useEffect(() => { - const defaultInitializer = getDefaultInitializer(contractArtifact); - setInitializer(defaultInitializer); - setFunctionAbis(getAllFunctionAbis(contractArtifact)); - }, [contractArtifact]); - - const handleParameterChange = (index, value) => { - parameters[index] = value; - setParameters(parameters); - }; - - const handleClose = () => { - onClose(); - }; - - const deploy = async () => { - setDeploying(true); - setLogsOpen(true); - - // TODO(#12081): Add contractArtifact.noirVersion and check here (via Noir.lock)? - - const deployer = new ContractDeployer(contractArtifact, wallet, PublicKeys.default(), initializer?.name); - - let args = []; - - if (initializer && parameters.length > 0) { - args = encodeArguments(initializer, parameters); - } - - const deployed = await deployer - .deploy(...args) - .send() - .wait(); - - onClose(deployed.contract.instance, alias); - }; - - return ( - - Deploy contract -
- {deploying ? ( - <> - Deploying... - - - ) : ( - <> - - - Initializer - - {initializer && - initializer.parameters.map((param, i) => ( - { - handleParameterChange(i, newValue); - }} - /> - ))} - - - { - setAlias(event.target.value); - }} - /> - - - - - - )} -
-
- ); -} diff --git a/playground/src/components/contract/dropzone.css b/playground/src/components/contract/components/dropzone.css similarity index 79% rename from playground/src/components/contract/dropzone.css rename to playground/src/components/contract/components/dropzone.css index 385470db582f..48f4ad0d9a06 100644 --- a/playground/src/components/contract/dropzone.css +++ b/playground/src/components/contract/components/dropzone.css @@ -1,4 +1,6 @@ .dropzone { + display: flex; + flex-direction: column; color: black; width: 100%; height: 100%; diff --git a/playground/src/components/contract/components/registerContractDialog.tsx b/playground/src/components/contract/components/registerContractDialog.tsx deleted file mode 100644 index f17bdc26b7c0..000000000000 --- a/playground/src/components/contract/components/registerContractDialog.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import DialogTitle from '@mui/material/DialogTitle'; -import Dialog from '@mui/material/Dialog'; -import { Contract, AztecAddress, type ContractInstanceWithAddress, type ContractArtifact } from '@aztec/aztec.js'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import FormControl from '@mui/material/FormControl'; -import FormGroup from '@mui/material/FormGroup'; -import TextField from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import { css } from '@mui/styled-engine'; -import { useContext, useState } from 'react'; -import { AztecContext } from '../../../aztecEnv'; - -const creationForm = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - padding: '1rem', - alignItems: 'center', -}); - -export function RegisterContractDialog({ - open, - contractArtifact, - onClose, -}: { - open: boolean; - contractArtifact: ContractArtifact; - onClose: (contract?: ContractInstanceWithAddress, alias?: string) => void; -}) { - const [alias, setAlias] = useState(''); - const [address, setAddress] = useState(''); - const [registering, setRegistering] = useState(false); - - const { wallet, node } = useContext(AztecContext); - - const handleClose = () => { - onClose(); - }; - - const register = async () => { - setRegistering(true); - - // TODO(#12081): Add contractArtifact.noirVersion and check here (via Noir.lock)? - - const contractInstance = await node.getContract(AztecAddress.fromString(address)); - - await wallet.registerContract({ - instance: contractInstance, - artifact: contractArtifact, - }); - - const contract = await Contract.at(AztecAddress.fromString(address), contractArtifact, wallet); - - onClose(contract.instance, alias); - }; - - return ( - - Register contract -
- {registering ? ( - <> - Registering... - - - ) : ( - <> - - - { - setAddress(event.target.value); - }} - /> - - - { - setAlias(event.target.value); - }} - /> - - - - - - )} -
-
- ); -} diff --git a/playground/src/components/contract/contract.tsx b/playground/src/components/contract/contract.tsx deleted file mode 100644 index 5ada511b3b83..000000000000 --- a/playground/src/components/contract/contract.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import { css } from '@mui/styled-engine'; -import { useDropzone } from 'react-dropzone'; -import './dropzone.css'; -import { useContext, useEffect, useState } from 'react'; -import { - AuthWitness, - Contract, - type ContractArtifact, - type ContractInstanceWithAddress, - loadContractArtifact, - getAllFunctionAbis, - type FunctionAbi, - FunctionType, -} from '@aztec/aztec.js'; -import { AztecContext } from '../../aztecEnv'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import CircularProgress from '@mui/material/CircularProgress'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormGroup from '@mui/material/FormGroup'; -import IconButton from '@mui/material/IconButton'; -import Input from '@mui/material/Input'; -import InputAdornment from '@mui/material/InputAdornment'; -import Typography from '@mui/material/Typography'; - -import FindInPageIcon from '@mui/icons-material/FindInPage'; -import { convertFromUTF8BufferAsString, formatFrAsString } from '../../utils/conversion'; -import { DeployContractDialog } from './components/deployContractDialog'; -import { FunctionParameter } from '../common/fnParameter'; -import ClearIcon from '@mui/icons-material/Clear'; -import { RegisterContractDialog } from './components/registerContractDialog'; -import { CopyToClipboardButton } from '../common/copyToClipboardButton'; -import VpnKeyIcon from '@mui/icons-material/VpnKey'; -import SendIcon from '@mui/icons-material/Send'; -import PsychologyIcon from '@mui/icons-material/Psychology'; -import { CreateAuthwitDialog } from './components/createAuthwitDialog'; -import { parse } from 'buffer-json'; - -const container = css({ - display: 'flex', - height: 'calc(100vh - 50px)', - width: '100%', - overflow: 'hidden', - justifyContent: 'center', - alignItems: 'center', -}); - -const dropZoneContainer = css({ - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '80%', - border: '3px dashed black', - borderRadius: '15px', - margin: '0rem 2rem 2rem 2rem', -}); - -const contractFnContainer = css({ - display: 'block', - width: '100%', - overflowY: 'auto', - color: 'black', - height: '100%', -}); - -const headerContainer = css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - flexWrap: 'wrap', - margin: '0 0.5rem', - padding: '0.1rem', - overflow: 'hidden', - justifyContent: 'stretch', - marginBottom: '0.5rem', -}); - -const header = css({ - display: 'flex', - width: '100%', - alignItems: 'center', - justifyContent: 'space-between', -}); - -const search = css({ - display: 'flex', - overflow: 'hidden', - '@media (width <= 800px)': { - width: '100%', - }, - '@media (width > 800px)': { - maxWidth: '500px', - }, -}); - -const contractActions = css({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const simulationContainer = css({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const checkBoxLabel = css({ - height: '1.5rem', - marginLeft: '-10px', -}); - -const loadingArtifactContainer = css({ - display: 'flex', - flexDirection: 'column', - textAlign: 'center', - alignItems: 'center', - justifyContent: 'center', - gap: '2rem', -}); - -const FORBIDDEN_FUNCTIONS = ['process_log', 'sync_notes', 'public_dispatch']; - -export function ContractComponent() { - const [contractArtifact, setContractArtifact] = useState(null); - const [functionAbis, setFunctionAbis] = useState([]); - - const [filters, setFilters] = useState({ - searchTerm: '', - private: true, - public: true, - utility: true, - }); - - const [isLoadingArtifact, setIsLoadingArtifact] = useState(false); - - const [isWorking, setIsWorking] = useState(false); - - const [simulationResults, setSimulationResults] = useState({}); - const [parameters, setParameters] = useState({}); - - const [openDeployContractDialog, setOpenDeployContractDialog] = useState(false); - const [openRegisterContractDialog, setOpenRegisterContractDialog] = useState(false); - const [openCreateAuthwitDialog, setOpenCreateAuthwitDialog] = useState(false); - const [authwitFnData, setAuthwitFnData] = useState({ - name: '', - parameters: [], - isPrivate: false, - }); - - const { - wallet, - walletDB, - currentContractAddress, - currentContract, - setCurrentContract, - setCurrentContractAddress, - setCurrentTx, - } = useContext(AztecContext); - - useEffect(() => { - const loadCurrentContract = async () => { - setIsLoadingArtifact(true); - const artifactAsString = await walletDB.retrieveAlias(`artifacts:${currentContractAddress}`); - const contractArtifact = loadContractArtifact(parse(convertFromUTF8BufferAsString(artifactAsString))); - const contract = await Contract.at(currentContractAddress, contractArtifact, wallet); - setCurrentContract(contract); - setContractArtifact(contract.artifact); - setFunctionAbis(getAllFunctionAbis(contract.artifact)); - setFilters({ - searchTerm: '', - private: true, - public: true, - utility: true, - }); - setIsLoadingArtifact(false); - }; - if (currentContractAddress && currentContract?.address !== currentContractAddress) { - loadCurrentContract(); - } - }, [currentContractAddress]); - - const { getRootProps, getInputProps } = useDropzone({ - onDrop: async files => { - const file = files[0]; - const reader = new FileReader(); - setIsLoadingArtifact(true); - reader.onload = async e => { - const contractArtifact = loadContractArtifact(JSON.parse(e.target?.result as string)); - setContractArtifact(contractArtifact); - setFunctionAbis(getAllFunctionAbis(contractArtifact)); - setIsLoadingArtifact(false); - }; - reader.readAsText(file); - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleParameterChange = (fnName: string, index: number, value: any) => { - const fnParameters = parameters[fnName] || []; - fnParameters[index] = value; - setParameters({ ...parameters, [fnName]: fnParameters }); - }; - - const handleContractCreation = async (contract?: ContractInstanceWithAddress, alias?: string) => { - if (contract && alias) { - await walletDB.storeContract(contract.address, contractArtifact, undefined, alias); - setCurrentContract(await Contract.at(contract.address, contractArtifact, wallet)); - setCurrentContractAddress(contract.address); - } - setOpenDeployContractDialog(false); - setOpenRegisterContractDialog(false); - }; - - const simulate = async (fnName: string) => { - setIsWorking(true); - let result; - try { - const fnParameters = parameters[fnName] ?? []; - const call = currentContract.methods[fnName](...fnParameters); - - result = await call.simulate(); - setSimulationResults({ - ...simulationResults, - ...{ [fnName]: { success: true, data: result } }, - }); - } catch (e) { - setSimulationResults({ - ...simulationResults, - ...{ [fnName]: { success: false, error: e.message } }, - }); - } - - setIsWorking(false); - }; - - const send = async (fnName: string) => { - setIsWorking(true); - let receipt; - let txHash; - const currentTx = { - status: 'proving' as const, - fnName: fnName, - contractAddress: currentContract.address, - }; - setCurrentTx(currentTx); - try { - const call = currentContract.methods[fnName](...parameters[fnName]); - - const provenCall = await call.prove(); - txHash = await provenCall.getTxHash(); - setCurrentTx({ - ...currentTx, - ...{ txHash, status: 'sending' }, - }); - receipt = await provenCall.send().wait({ dontThrowOnRevert: true }); - await walletDB.storeTx({ - contractAddress: currentContract.address, - txHash, - fnName, - receipt, - }); - setCurrentTx({ - ...currentTx, - ...{ - txHash, - status: receipt.status, - receipt, - error: receipt.error, - }, - }); - } catch (e) { - console.error(e); - setCurrentTx({ - ...currentTx, - ...{ - txHash, - status: 'error', - error: e.message, - }, - }); - } - - setIsWorking(false); - }; - - const handleAuthwitFnDataChanged = ( - fnName: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters: any[], - isPrivate: boolean, - ) => { - setAuthwitFnData({ name: fnName, parameters, isPrivate }); - setOpenCreateAuthwitDialog(true); - }; - - const handleAuthwitCreation = async (witness?: AuthWitness, alias?: string) => { - if (witness && alias) { - await walletDB.storeAuthwitness(witness, undefined, alias); - } - setAuthwitFnData({ name: '', parameters: [], isPrivate: false }); - setOpenCreateAuthwitDialog(false); - }; - - return ( -
- {!contractArtifact ? ( - !isLoadingArtifact ? ( -
-
- - Drag 'n' drop some files here, or click to select files -
-
- ) : ( -
- Loading artifact... - -
- ) - ) : ( -
-
-
- - {contractArtifact.name} - - {!currentContract && wallet && ( -
- - - - -
- )} - {currentContract && ( -
- {formatFrAsString(currentContract.address.toString())} - - { - setCurrentContractAddress(null); - setCurrentContract(null); - setContractArtifact(null); - }} - > - - -
- )} -
-
- - setFilters({ ...filters, searchTerm: e.target.value })} - endAdornment={ - - - - } - /> -
- setFilters({ ...filters, private: e.target.checked })} - /> - } - label="Private" - /> - setFilters({ ...filters, public: e.target.checked })} - /> - } - label="Public" - /> - - setFilters({ - ...filters, - utility: e.target.checked, - }) - } - /> - } - label="Utility" - /> -
-
-
-
- {functionAbis - .filter( - fn => - !fn.isInternal && - !FORBIDDEN_FUNCTIONS.includes(fn.name) && - ((filters.private && fn.functionType === FunctionType.PRIVATE) || - (filters.public && fn.functionType === FunctionType.PUBLIC) || - (filters.utility && fn.functionType === FunctionType.UTILITY)) && - (filters.searchTerm === '' || fn.name.includes(filters.searchTerm)), - ) - .map(fn => ( - - - - {fn.functionType} - - - {fn.name} - - {fn.parameters.length > 0 && ( - <> - - Parameters - - - {fn.parameters.map((param, i) => ( - { - handleParameterChange(fn.name, i, newValue); - }} - /> - ))} - - - )} - - {!isWorking && simulationResults[fn.name] !== undefined && ( -
- - Simulation results:  - - {simulationResults[fn.name].success ? ( - - {simulationResults?.[fn.name]?.data.length === 0 - ? '-' - : simulationResults?.[fn.name].data.toString()} - - ) : ( - - {simulationResults?.[fn.name]?.error} - - )}{' '} -
- )} - {isWorking ? : <>} -
- - - - - -
- ))} -
- )} - -
- ); -} diff --git a/playground/src/components/contract/styles.ts b/playground/src/components/contract/styles.ts new file mode 100644 index 000000000000..30d383db8665 --- /dev/null +++ b/playground/src/components/contract/styles.ts @@ -0,0 +1,322 @@ +import { css } from '@mui/styled-engine'; + +// Base styles +const baseButton = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '16px 20px', + gap: '9px', + height: '38px', + background: '#9894FF', + borderRadius: '8px', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '16px', + lineHeight: '19px', + color: '#000000', + border: 'none', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#8C7EFF', + }, + '&:disabled': { + backgroundColor: '#CDD1D5', + color: '#808080', + cursor: 'not-allowed', + } +}); + +const baseLabel = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '6px 16px', + gap: '10px', + background: '#9894FF', + borderRadius: '30px', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 500, + fontSize: '12px', + lineHeight: '120%', + letterSpacing: '0.1em', + textTransform: 'uppercase', + color: '#FFFFFF', + marginBottom: '10px', +}); + +// Layout styles +export const container = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + background: '#E9E9E9', + borderRadius: '10px', + padding: '45px', + overflow: 'hidden', + '@media (max-width: 1100px)': { + width: 'auto', + padding: '24px', + }, +}); + +export const headerSection = css({ + width: '100%', + marginBottom: '24px', +}); + +export const descriptionText = css({ + fontFamily: '"Space Grotesk", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '18px', + lineHeight: '120%', + display: 'flex', + alignItems: 'center', + textAlign: 'center', + color: '#000000', + marginBottom: '25px', + width: '100%', +}); + +export const buttonContainer = css({ + display: 'flex', + justifyContent: 'center', + gap: '24px', + marginBottom: '25px', +}); + +export const actionButton = css({ + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '20px 32px', + gap: '8px', + width: '230px', + height: '56px', + background: '#CDD1D5', + borderRadius: '12px', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 600, + fontSize: '17px', + lineHeight: '16px', + color: '#000000', + '&:hover': { + backgroundColor: '#BCC0C4', + } +}); + +export const dropZoneContainer = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '80%', + border: '3px dashed #9894FF', + borderRadius: '15px', + margin: '2rem 0', + backgroundColor: 'rgba(152, 148, 255, 0.04)', + alignItems: 'center', + justifyContent: 'center', +}); + +export const uploadIcon = css({ + fontSize: '64px', + color: '#9894FF', + marginBottom: '1rem', +}); + +export const contractFnContainer = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + flex: '1 1 auto', + height: '0', + minHeight: '0', + overflow: 'auto', +}); + +export const tokenSection = css({ + marginTop: '50px', + marginBottom: '25px', +}); + +export const tokenHeader = css({ + fontFamily: '"Space Grotesk", sans-serif', + fontStyle: 'normal', + fontWeight: 700, + fontSize: '48px', + lineHeight: '100%', + display: 'flex', + alignItems: 'center', + letterSpacing: '0.02em', + color: '#2D2D2D', + marginBottom: '25px', +}); + +export const searchContainer = css({ + width: '361px', + height: '36px', + background: 'rgba(250, 250, 250, 0.93)', + borderRadius: '6px', + display: 'flex', + alignItems: 'center', + padding: '8px', + marginBottom: '15px', +}); + +export const filterContainer = css({ + display: 'flex', + flexDirection: 'row', + gap: '7px', + marginBottom: '25px', +}); + +export const filterButton = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '6px 5px', + gap: '11px', + height: '36px', + background: '#CDD1D5', + borderRadius: '6px', + cursor: 'pointer', + position: 'relative', +}); + +export const filterCheckbox = css({ + width: '24px', + height: '24px', + background: '#CDD1D5', + border: '2px solid rgba(255, 255, 255, 0.2)', + borderRadius: '6px', + marginLeft: '5px', +}); + +export const filterLabel = css({ + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 500, + fontSize: '16px', + lineHeight: '19px', + textAlign: 'center', + color: '#000000', +}); + +export const filterHelpIcon = css({ + fontSize: '16px', + marginLeft: '4px', + color: '#666', + display: 'none', +}); + +export const functionCard = css({ + boxSizing: 'border-box', + width: '100%', + background: '#CDD1D5', + border: '2px solid #DEE2E6', + borderRadius: '20px', + marginBottom: '20px', + overflow: 'hidden', +}); + +export const functionTypeLabel = css(baseLabel, { + width: '88px', + height: '20px', +}); + +export const functionName = css({ + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 600, + fontSize: '22px', + lineHeight: '100%', + display: 'flex', + alignItems: 'center', + letterSpacing: '0.02em', + color: '#2D2D2D', + marginBottom: '10px', +}); + +export const functionDescription = css({ + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '120%', + color: '#4A4A4A', + marginBottom: '20px', +}); + +export const parametersLabel = css(baseLabel, { + width: '123px', + height: '20px', +}); + +export const parameterInput = css({ + background: '#FFFFFF', + border: '2px solid #DEE2E6', + borderRadius: '8px', + height: '48px', + padding: '0 24px', + display: 'flex', + alignItems: 'center', + marginRight: '16px', + marginBottom: '16px', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 600, + fontSize: '16px', + lineHeight: '19px', + color: '#3F444A', + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiInputBase-root': { + '&.Mui-focused fieldset': { + border: 'none', + } + } +}); + +export const actionButtonsContainer = css({ + display: 'flex', + flexDirection: 'row', + gap: '12px', + marginTop: '15px', +}); + +export const simulateButton = css(baseButton); +export const sendButton = css(baseButton); +export const authwitButton = css(baseButton); + +export const loadingArtifactContainer = css({ + display: 'flex', + flexDirection: 'column', + textAlign: 'center', + alignItems: 'center', + justifyContent: 'center', + gap: '2rem', + height: '100%', +}); + +export const headerContainer = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + marginBottom: '25px', +}); + +export const functionListContainer = css({ + width: '100%', + padding: '0', +}); diff --git a/playground/src/components/home/Home.tsx b/playground/src/components/home/Home.tsx new file mode 100644 index 000000000000..2bd02afffef1 --- /dev/null +++ b/playground/src/components/home/Home.tsx @@ -0,0 +1,188 @@ +import { css } from '@emotion/react'; +import { ContractComponent } from '../contract/Contract'; +import { SidebarComponent } from '../sidebar/Sidebar'; +import { useState } from 'react'; +import { AztecContext } from '../../aztecEnv'; +import { LogPanel } from '../logPanel/LogPanel'; +import { Landing } from './components/Landing'; +import logoURL from '../../assets/aztec_logo.png'; +import { LoadingModal } from '../common/LoadingModal'; + +const layout = css({ + display: 'flex', + flexDirection: 'column', + height: '100%', + overflow: 'hidden', + width: '100%', + flex: 1, +}); + +const contentLayout = css({ + display: 'flex', + flexDirection: 'row', + position: 'relative', + flexShrink: 0, + height: 'calc(100% - 220px)', + minHeight: 0, + overflow: 'auto', + margin: '24px 60px', + '@media (max-width: 1200px)': { + height: 'calc(100% - 150px)', + flexDirection: 'column', + margin: '0 12px', + }, +}); + +const headerFrame = css({ + height: '100px', + margin: '24px 60px', + backgroundColor: '#CDD1D5', + borderRadius: '10px', + display: 'flex', + alignItems: 'center', + padding: '0 40px', + position: 'relative', + '@media (max-width: 1200px)': { + margin: '12px 12px 24px 12px', + padding: '0 12px', + height: '80px', + }, +}); + +const logo = css({ + height: '60px', + objectFit: 'contain', + marginRight: '2rem', + '@media (max-width: 1200px)': { + height: 'auto', + width: '120px', + marginRight: '0.1rem', + }, +}); + +const headerTitle = css({ + fontFamily: '"Space Grotesk", sans-serif', + fontStyle: 'normal', + fontWeight: 500, + fontSize: '42px', + lineHeight: '48px', + display: 'flex', + alignItems: 'center', + letterSpacing: '0.03em', + color: '#2D2D2D', + textDecoration: 'none', + marginTop: '0.5rem', + padding: '1rem', + '@media (max-width: 1200px)': { + marginTop: '0.3rem', + fontSize: '20px', + lineHeight: '20px', + }, +}); + +const docsButton = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '12px 24px', + position: 'absolute', + width: '160px', + height: '42px', + right: '40px', + background: '#8C7EFF', + boxShadow: '0px 0px 0px 1px #715EC2, 0px 0px 0px 3px rgba(247, 249, 255, 0.08)', + borderRadius: '6px', + color: '#FFFFFF', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 500, + fontSize: '16px', + lineHeight: '20px', + cursor: 'pointer', + textDecoration: 'none', + '@media (max-width: 1200px)': { + padding: 0, + fontSize: '14px', + width: '85px', + gap: 0, + right: '12px', + }, +}); + +export default function Home() { + const [pxe, setPXE] = useState(null); + const [wallet, setWallet] = useState(null); + const [showContractInterface, setShowContractInterface] = useState(false); + const [node, setAztecNode] = useState(null); + const [isPXEInitialized, setPXEInitialized] = useState(false); + const [walletAlias, setWalletAlias] = useState(''); + const [walletDB, setWalletDB] = useState(null); + const [currentContractArtifact, setCurrentContractArtifact] = useState(null); + const [currentTx, setCurrentTx] = useState(null); + const [currentContractAddress, setCurrentContractAddress] = useState(null); + const [logs, setLogs] = useState([]); + const [logsOpen, setLogsOpen] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); + const [connecting, setConnecting] = useState(false); + const [network, setNetwork] = useState(null); + + const AztecContextInitialValue = { + pxe, + connecting, + network, + wallet, + isPXEInitialized, + walletAlias, + walletDB, + currentContractArtifact, + currentTx, + node, + currentContractAddress, + logs, + logsOpen, + drawerOpen, + showContractInterface, + setNetwork, + setConnecting, + setDrawerOpen, + setLogsOpen, + setLogs, + setAztecNode, + setCurrentTx, + setWalletDB, + setPXEInitialized, + setWallet, + setPXE, + setShowContractInterface, + setWalletAlias, + setCurrentContractArtifact, + setCurrentContractAddress, + }; + + return ( +
+
+ Aztec Logo +
PLAYGROUND
+ + Inspiration + +
+ +
+ + {showContractInterface ? : } +
+ + +
+
+ ); +} diff --git a/playground/src/components/home/components/Landing.tsx b/playground/src/components/home/components/Landing.tsx new file mode 100644 index 000000000000..b83e65ad4c40 --- /dev/null +++ b/playground/src/components/home/components/Landing.tsx @@ -0,0 +1,394 @@ +import { css } from '@emotion/react'; +import welcomeIconURL from '../../../assets/welcome_icon.svg'; + +const landingPage = css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-start', + padding: '0', + width: '100%', + height: 'auto', + minHeight: '100%', + flex: 1, +}); + +const cardsContainer = css({ + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: '24px', + width: '100%', + margin: '0 auto', + '@media (max-width: 900px)': { + gridTemplateColumns: 'repeat(1, 1fr)', + }, + '@media (min-width: 901px) and (max-width: 1100px)': { + gridTemplateColumns: 'repeat(2, 1fr)', + }, +}); + +const featureCard = css({ + background: '#CDD1D5', + borderRadius: '20px', + padding: '25px', + height: '250px', + display: 'flex', + flexDirection: 'column', +}); + +const cardIcon = css({ + width: '50px', + height: '50px', + marginBottom: '35px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +const cardTitle = css({ + fontFamily: '"Space Grotesk", sans-serif', + fontWeight: 700, + fontSize: '24px', + lineHeight: '100%', + letterSpacing: '0.02em', + color: '#2D2D2D', + marginBottom: '12px', +}); + +const cardDescription = css({ + fontFamily: 'Inter, sans-serif', + fontWeight: 400, + fontSize: '14px', + lineHeight: '110%', + letterSpacing: '0.01em', + color: 'rgba(0, 0, 0, 0.8)', +}); + +const contentFrame = css({ + width: '100%', + backgroundColor: '#E9E9E9', + borderRadius: '10px', + padding: '45px', + position: 'relative', + display: 'flex', + flexDirection: 'column', + flex: 1, + '@media (max-width: 1100px)': { + width: 'auto', + margin: '24px 0 48px 0', + padding: '24px', + }, +}); + +const welcomeSection = css({ + width: '100%', + height: '260px', + backgroundColor: '#CDD1D5', + borderRadius: '20px', + position: 'relative', + display: 'flex', + margin: '0 auto 24px auto', + '@media (max-width: 1000px)': { + width: '100%', + height: 'auto', + flexDirection: 'column', + padding: '20px', + }, +}); + +const welcomeContent = css({ + padding: '39px', + width: '60%', + '@media (max-width: 1000px)': { + width: '100%', + padding: '20px', + }, +}); + +const welcomeTitle = css({ + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 600, + fontSize: '22px', + lineHeight: '130%', + display: 'flex', + alignItems: 'center', + color: '#2D2D2D', + marginBottom: '16px', +}); + +const welcomeText = css({ + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '16px', + lineHeight: '135%', + display: 'flex', + alignItems: 'center', + color: '#1E1E1E', + maxWidth: '558px', +}); + +const mainContent = css({ + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + overflow: 'auto', + margin: '0 60px 0 24px', + '@media (max-width: 1200px)': { + minHeight: 'auto', + margin: '0 24px', + }, +}); + +const getStartedButton = css({ + width: '205px', + height: '56px', + backgroundColor: '#CDD1D5', + borderRadius: '12px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontFamily: 'Inter, sans-serif', + fontWeight: 600, + fontSize: '17px', + lineHeight: '16px', + margin: '20px auto 0', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#BCC0C4', + }, +}); + +const sidebarButton = css({ + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: '15px 32px', + gap: '8px', + width: '230px', + height: '50px', + margin: '8px auto', + background: '#CDD1D5', + borderRadius: '12px', + fontFamily: 'Inter, sans-serif', + fontStyle: 'normal', + fontWeight: 600, + fontSize: '17px', + lineHeight: '16px', + color: '#000000', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#BCC0C4', + }, +}); + +const sidebarButtonActive = css({ + background: '#9894FF', + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#8C7EFF', + }, +}); + +const flashAnimation = css({ + animation: 'flash 1s ease-in-out', + '@keyframes flash': { + '0%': { backgroundColor: '#E9E9E9' }, + '50%': { backgroundColor: '#9894FF' }, + '100%': { backgroundColor: '#E9E9E9' }, + }, +}); + +// Account Abstraction icon +const AccountAbstractionIcon = () => ( +
+
+
+
+
+
+); + +// Private Voting icon +const PrivateVotingIcon = () => ( +
+
+
+
+
+); + +// Private Tokens icon +const PrivateTokensIcon = () => ( +
+
+
+
+
+
+); + +export function Landing() { + return ( +
+
+
+
+
Welcome to the Playground
+
+ Playground is a web-app for interacting with Aztec. Create an aztec account, try one of our default + contracts or upload your own and interact with it while creating client side proofs in the browser! It is + a minimalistic remix.ethereum.org but for Aztec +
+
+
+ Welcome visualization +
+
+ +
+
+
+ +
+
Account Abstraction
+
Short description of what account abstraction is and how it's being used
+
+ +
+
+ +
+
Private Voting
+
Short description of how a user could setup private voting
+
+ +
+
+ +
+
Private Tokens
+
Short description of what is possible with private tokens
+
+
+ +
Get Started
+
+
+ ); +} diff --git a/playground/src/components/home/home.tsx b/playground/src/components/home/home.tsx deleted file mode 100644 index 697d5ae87763..000000000000 --- a/playground/src/components/home/home.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { css } from '@emotion/react'; -import { ContractComponent } from '../contract/contract'; -import { SidebarComponent } from '../sidebar/sidebar'; -import { useEffect, useState } from 'react'; -import { AztecContext, AztecEnv } from '../../aztecEnv'; -import { LogPanel } from '../logPanel/logPanel'; -import logoURL from '../../assets/Aztec_logo.png'; -import Drawer from '@mui/material/Drawer'; -import LinearProgress from '@mui/material/LinearProgress'; - -const layout = css({ - display: 'flex', - flexDirection: 'row', - height: '100%', -}); - -const logo = css({ - width: '100%', - padding: '0.5rem', -}); - -const collapsedDrawer = css({ - height: '100%', - width: '4rem', - backgroundColor: 'var(--mui-palette-primary-light)', - overflow: 'hidden', -}); - -export default function Home() { - const [pxe, setPXE] = useState(null); - const [wallet, setWallet] = useState(null); - const [nodeURL, setNodeURL] = useState(''); - const [node, setAztecNode] = useState(null); - const [isPXEInitialized, setPXEInitialized] = useState(false); - const [walletAlias, setWalletAlias] = useState(''); - const [walletDB, setWalletDB] = useState(null); - const [currentContract, setCurrentContract] = useState(null); - const [currentTx, setCurrentTx] = useState(null); - const [currentContractAddress, setCurrentContractAddress] = useState(null); - const [logs, setLogs] = useState([]); - const [logsOpen, setLogsOpen] = useState(false); - const [drawerOpen, setDrawerOpen] = useState(false); - - const [isNetworkStoreInitialized, setIsNetworkStoreInitialized] = useState(false); - - useEffect(() => { - const initNetworkStore = async () => { - await AztecEnv.initNetworkStore(); - setIsNetworkStoreInitialized(true); - }; - initNetworkStore(); - }, []); - - const AztecContextInitialValue = { - pxe, - nodeURL, - wallet, - isPXEInitialized, - walletAlias, - walletDB, - currentContract, - currentTx, - node, - currentContractAddress, - logs, - logsOpen, - drawerOpen, - setDrawerOpen, - setLogsOpen, - setLogs, - setAztecNode, - setCurrentTx, - setWalletDB, - setPXEInitialized, - setWallet, - setPXE, - setNodeURL, - setWalletAlias, - setCurrentContract, - setCurrentContractAddress, - }; - - return ( -
- -
setDrawerOpen(!drawerOpen)}> - -
- setDrawerOpen(false)} - variant="temporary" - open={drawerOpen} - > - {isNetworkStoreInitialized ? : } - - - -
-
- ); -} diff --git a/playground/src/components/logPanel/logPanel.tsx b/playground/src/components/logPanel/LogPanel.tsx similarity index 96% rename from playground/src/components/logPanel/logPanel.tsx rename to playground/src/components/logPanel/LogPanel.tsx index 07e26250e310..b4dcfb170e8a 100644 --- a/playground/src/components/logPanel/logPanel.tsx +++ b/playground/src/components/logPanel/LogPanel.tsx @@ -62,8 +62,6 @@ const logContent = css({ }), }); -const logTimestamp = css({}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const safeStringify = (obj: any) => JSON.stringify(obj, (_, v) => (typeof v === 'bigint' ? v.toString() : v)); @@ -119,7 +117,7 @@ export function LogPanel() { {logs.map((log, index) => (
- {log.prefix}:  + {log.prefix}
@@ -127,7 +125,7 @@ export function LogPanel() { {safeStringify(log.data)}
-
+
+{log.timestamp - (logs[index + 1]?.timestamp ?? log.timestamp)} ms diff --git a/playground/src/components/sidebar/Sidebar.tsx b/playground/src/components/sidebar/Sidebar.tsx new file mode 100644 index 000000000000..b0c7d7551319 --- /dev/null +++ b/playground/src/components/sidebar/Sidebar.tsx @@ -0,0 +1,172 @@ +import { css } from '@mui/styled-engine'; +import { AztecContext } from '../../aztecEnv'; +import { AztecAddress } from '@aztec/aztec.js'; +import { useContext, useEffect, useState } from 'react'; +import Typography from '@mui/material/Typography'; +import { formatFrAsString, parseAliasedBuffersAsString } from '../../utils/conversion'; +import { TxsPanel } from './components/TxsPanel'; +import { NetworkSelector } from './components/NetworkSelector'; +import { AccountSelector } from './components/AccountSelector'; +import { AddressBook } from './components/AddressBook'; +import { ContractSelector } from './components/ContractSelector'; +import { ButtonWithModal } from './components/ButtonWithModal'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { IconButton } from '@mui/material'; +import { dropdownIcon } from '../../styles/common'; + +const container = css({ + width: '25%', + height: '100%', + position: 'relative', + backgroundColor: '#E9E9E9', + overflow: 'auto', + flexShrink: 0, + flexGrow: 0, + borderRadius: '10px', + display: 'flex', + flexDirection: 'column', + transition: 'all 0.3s ease-out', + padding: '20px', + margin: '0 24px 0 0', + '@media (max-width: 1200px)': { + padding: '12px', + width: 'auto', + maxHeight: '350px', + margin: '0 0 12px 0', + }, +}); + +export function SidebarComponent() { + const { + connecting, + network, + wallet, + walletDB, + currentContractAddress, + currentContractArtifact, + drawerOpen, + setDrawerOpen, + } = useContext(AztecContext); + + const [isNetworkConnected, setIsNetworkConnected] = useState(false); + const [walletAlias, setWalletAlias] = useState(undefined); + const [contractAlias, setContractAlias] = useState(undefined); + + const [smallScreen, setSmallScreen] = useState(window.matchMedia('(max-width: 1200px)').matches); + + useEffect(() => { + window.matchMedia('(max-width: 1200px)').addEventListener('change', e => setSmallScreen(e.matches)); + }, []); + + useEffect(() => { + setDrawerOpen(!smallScreen); + }, [smallScreen]); + + useEffect(() => { + setIsNetworkConnected(!connecting && !!network?.nodeURL); + }, [connecting, network]); + + useEffect(() => { + const refreshAlias = async () => { + const aliasedBuffers = await walletDB.listAliases('accounts'); + const aliasedAccounts = parseAliasedBuffersAsString(aliasedBuffers); + const alias = aliasedAccounts.find(({ value }) => wallet.getAddress().equals(AztecAddress.fromString(value))); + setWalletAlias(alias?.key.replace('accounts:', '')); + }; + + if (wallet && walletDB) { + refreshAlias(); + } + }, [wallet]); + + useEffect(() => { + const refreshContracts = async () => { + const aliasedBuffers = await walletDB.listAliases('contracts'); + const aliasedContracts = parseAliasedBuffersAsString(aliasedBuffers); + const alias = aliasedContracts.find(({ value }) => + currentContractAddress?.equals(AztecAddress.fromString(value)), + ); + setContractAlias(alias?.key.replace('contracts:', '')); + }; + + if (walletDB) { + refreshContracts(); + } + }); + + const [activeSection, setActiveSection] = useState('network'); + + const handleSectionToggle = (section: 'network' | 'account' | 'contract') => { + // Toggle off when clicking the same section + setActiveSection(activeSection === section ? '' : section); + }; + + // Get button text based on connection state + const getNetworkButtonText = () => { + if (connecting) return 'Connecting...'; + if (isNetworkConnected) return `Connected to ${network.name}`; + return 'Connect to Network'; + }; + + const getAccountButtonText = () => { + if (!wallet) return 'Connect Account'; + return `${walletAlias || formatFrAsString(wallet.getAddress().toString())} Account`; + }; + + const getContractButtonText = () => { + if (!currentContractArtifact) return 'Select Contract'; + const name = currentContractArtifact.name; + if (currentContractAddress) { + return `${contractAlias ?? name} (${formatFrAsString(currentContractAddress.toString())})`; + } else { + return name; + } + }; + + return ( +
+
+ Tools + {smallScreen && ( + setDrawerOpen(!drawerOpen)}> + + + )} +
+ {drawerOpen && ( + <> + handleSectionToggle('network')} + > + + + handleSectionToggle('account')} + > + + + handleSectionToggle('contract')} + > + + +
+ + + + )} +
+ ); +} diff --git a/playground/src/components/sidebar/components/AccountSelector.tsx b/playground/src/components/sidebar/components/AccountSelector.tsx new file mode 100644 index 000000000000..66b8a4f644b0 --- /dev/null +++ b/playground/src/components/sidebar/components/AccountSelector.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect, useContext } from 'react'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { type SelectChangeEvent } from '@mui/material/Select'; +import Typography from '@mui/material/Typography'; +import AddIcon from '@mui/icons-material/Add'; +import CircularProgress from '@mui/material/CircularProgress'; +import { CreateAccountDialog } from './CreateAccountDialog'; +import { CopyToClipboardButton } from '../../common/CopyToClipboardButton'; +import { AztecAddress, type DeployOptions, AccountWalletWithSecretKey, DeployMethod } from '@aztec/aztec.js'; +import { getSchnorrAccount } from '@aztec/accounts/schnorr/lazy'; + +import { + convertFromUTF8BufferAsString, + formatFrAsString, + parseAliasedBuffersAsString, +} from '../../../utils/conversion'; +import { getEcdsaRAccount, getEcdsaKAccount } from '@aztec/accounts/ecdsa/lazy'; + +import { Fq, type AccountManager } from '@aztec/aztec.js'; +import { css } from '@emotion/react'; +import { AztecContext } from '../../../aztecEnv'; +import { getInitialTestAccounts } from '@aztec/accounts/testing/lazy'; +import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { useTransaction } from '../../../hooks/useTransaction'; +import { select } from '../../../styles/common'; + +const modalContainer = css({ + padding: '10px 0', +}); + +const loadingContainer = css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + padding: '20px 0', + gap: '12px', +}); + +export function AccountSelector() { + const [openCreateAccountDialog, setOpenCreateAccountDialog] = useState(false); + const [isAccountsLoading, setIsAccountsLoading] = useState(true); + const [accounts, setAccounts] = useState([]); + + const [isOpen, setIsOpen] = useState(false); + + const { setWallet, wallet, walletDB, isPXEInitialized, pxe, network } = useContext(AztecContext); + + const { sendTx } = useTransaction(); + + const getAccounts = async () => { + const aliasedBuffers = await walletDB.listAliases('accounts'); + const aliasedAccounts = parseAliasedBuffersAsString(aliasedBuffers); + const testAccountData = network.hasTestAccounts ? await getInitialTestAccounts() : []; + let i = 0; + for (const accountData of testAccountData) { + const account: AccountManager = await getSchnorrAccount( + pxe, + accountData.secret, + accountData.signingKey, + accountData.salt, + ); + if (!aliasedAccounts.find(({ value }) => account.getAddress().equals(AztecAddress.fromString(value)))) { + await account.register(); + const instance = account.getInstance(); + const wallet = await account.getWallet(); + const alias = `test${i}`; + await walletDB.storeAccount(instance.address, { + type: 'schnorr', + secretKey: wallet.getSecretKey(), + alias, + signingKey: deriveSigningKey(wallet.getSecretKey()), + salt: account.getInstance().salt, + }); + aliasedAccounts.push({ + key: `accounts:${alias}`, + value: instance.address.toString(), + }); + } + i++; + } + return aliasedAccounts; + }; + + useEffect(() => { + const refreshAccounts = async () => { + setIsAccountsLoading(true); + const accounts = await getAccounts(); + setAccounts(accounts); + setIsAccountsLoading(false); + }; + if (walletDB && pxe) { + refreshAccounts(); + } + }, [wallet, walletDB, pxe]); + + const handleAccountChange = async (event: SelectChangeEvent) => { + if (event.target.value == '') { + return; + } + setIsAccountsLoading(true); + const accountAddress = AztecAddress.fromString(event.target.value); + const accountData = await walletDB.retrieveAccount(accountAddress); + const type = convertFromUTF8BufferAsString(accountData.type); + let accountManager: AccountManager; + switch (type) { + case 'schnorr': { + accountManager = await getSchnorrAccount( + pxe, + accountData.secretKey, + Fq.fromBuffer(accountData.signingKey), + accountData.salt, + ); + break; + } + case 'ecdsasecp256r1': { + accountManager = await getEcdsaRAccount(pxe, accountData.secretKey, accountData.signingKey, accountData.salt); + break; + } + case 'ecdsasecp256k1': { + accountManager = await getEcdsaKAccount(pxe, accountData.secretKey, accountData.signingKey, accountData.salt); + break; + } + default: { + throw new Error('Unknown account type'); + } + } + await accountManager.register(); + setWallet(await accountManager.getWallet()); + setIsAccountsLoading(false); + }; + + const handleAccountCreation = async ( + accountWallet?: AccountWalletWithSecretKey, + publiclyDeploy?: boolean, + interaction?: DeployMethod, + opts?: DeployOptions, + ) => { + setOpenCreateAccountDialog(false); + setIsAccountsLoading(true); + if (accountWallet && publiclyDeploy) { + const deploymentResult = await sendTx(`Deployment of account`, interaction, accountWallet.getAddress(), opts); + if (deploymentResult) { + setAccounts([ + ...accounts, + { key: `accounts:${accountWallet.getAddress()}`, value: accountWallet.getAddress().toString() }, + ]); + setWallet(accountWallet); + } else { + // Temporarily remove from accounts if deployment fails + await walletDB.deleteAccount(accountWallet.getAddress()); + } + } + setIsAccountsLoading(false); + }; + + // Render loading state if accounts are being loaded + if (isAccountsLoading) { + return ( +
+
+ + {!isPXEInitialized ? 'Not connected...' : 'Loading accounts...'} +
+
+ ); + } + + // If PXE is not initialized or network is not connected, show a message + if (!isPXEInitialized) { + return ( +
+ + Note: Connect to a network first to create and use accounts + +
+ ); + } + + return ( +
+ + Account + + {wallet && !isAccountsLoading && ( + + )} + + +
+ ); +} diff --git a/playground/src/components/sidebar/components/addNetworkDialog.tsx b/playground/src/components/sidebar/components/AddNetworkDialog.tsx similarity index 100% rename from playground/src/components/sidebar/components/addNetworkDialog.tsx rename to playground/src/components/sidebar/components/AddNetworkDialog.tsx diff --git a/playground/src/components/sidebar/components/addSenderDialog.tsx b/playground/src/components/sidebar/components/AddSenderDialog.tsx similarity index 100% rename from playground/src/components/sidebar/components/addSenderDialog.tsx rename to playground/src/components/sidebar/components/AddSenderDialog.tsx diff --git a/playground/src/components/sidebar/components/AddressBook.tsx b/playground/src/components/sidebar/components/AddressBook.tsx new file mode 100644 index 000000000000..e9ac1febb523 --- /dev/null +++ b/playground/src/components/sidebar/components/AddressBook.tsx @@ -0,0 +1,33 @@ +import { useContext, useState } from 'react'; +import { AddSendersDialog } from './AddSenderDialog'; +import Button from '@mui/material/Button'; +import ContactsIcon from '@mui/icons-material/Contacts'; +import { AztecContext } from '../../../aztecEnv'; +import type { AztecAddress } from '@aztec/aztec.js'; + +export function AddressBook() { + const [openAddSendersDialog, setOpenAddSendersDialog] = useState(false); + + const { wallet, walletDB, isPXEInitialized } = useContext(AztecContext); + + const handleSenderAdded = async (sender?: AztecAddress, alias?: string) => { + if (sender && alias) { + await wallet.registerSender(sender); + await walletDB.storeAlias('senders', alias, Buffer.from(sender.toString())); + } + setOpenAddSendersDialog(false); + }; + + return ( + <> + {wallet && walletDB && isPXEInitialized && ( + <> + + + + )} + + ); +} diff --git a/playground/src/components/sidebar/components/ButtonWithModal.tsx b/playground/src/components/sidebar/components/ButtonWithModal.tsx new file mode 100644 index 000000000000..e4bd0d0f628b --- /dev/null +++ b/playground/src/components/sidebar/components/ButtonWithModal.tsx @@ -0,0 +1,130 @@ +import { useRef } from 'react'; +import type { ReactNode } from 'react'; +import { css, keyframes } from '@emotion/react'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { dropdownIcon } from '../../../styles/common'; + +// Animation for the modal appearing +const popupAnimation = keyframes` + from { + opacity: 0; + max-height: 0; + transform: scaleY(0); + transform-origin: top; + margin-bottom: 0; + } + to { + opacity: 1; + max-height: 500px; + transform: scaleY(1); + transform-origin: top; + margin-bottom: 15px; + } +`; + +// Styles for the container +const containerStyle = css({ + position: 'relative', + width: '100%', + marginBottom: '15px', + transition: 'transform 0.3s ease', +}); + +// Styles for the button +const buttonStyle = css({ + height: '56px', + borderRadius: '12px', + backgroundColor: '#CDD1D5', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '12px 24px', + fontFamily: "'Inter', sans-serif", + fontWeight: 600, + fontSize: '15px', + lineHeight: '15px', + color: '#000000', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#BCC0C4', + }, + '@media (max-width: 400px)': { + width: '100%', + }, +}); + +// Styles for active button +const activeButtonStyle = css({ + background: '#9894FF', + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#8985FF', + }, +}); + +// Styles for the modal content wrapper +const modalStyle = css({ + position: 'relative', + left: '0', + right: '0', + marginTop: '5px', + backgroundColor: '#F5F5F5', + borderRadius: '10px', + padding: '15px', + boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.1)', + zIndex: 10, + animation: `${popupAnimation} 0.3s ease-out forwards`, + overflow: 'hidden', + transformOrigin: 'top', +}); + +// Styles for loading spinner +const loadingSpinner = css({ + marginLeft: '8px', + color: '#FFFFFF', +}); + +interface ButtonWithModalProps { + label: string; + isActive: boolean; + isSelected?: boolean; + connectionStatus?: string; + onClick: () => void; + children?: ReactNode; +} + +export function ButtonWithModal({ + label, + isActive, + isSelected = false, + connectionStatus, + onClick, + children, +}: ButtonWithModalProps) { + const modalRef = useRef(null); + + const handleButtonClick = () => { + onClick(); + }; + + const handleModalClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( +
+ {/* Button */} +
+ {isSelected && connectionStatus ? connectionStatus : label} + +
+ + {/* Modal - show whenever isActive is true */} + {isActive && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/playground/src/components/sidebar/components/ContractSelector.tsx b/playground/src/components/sidebar/components/ContractSelector.tsx new file mode 100644 index 000000000000..4abcffa989a8 --- /dev/null +++ b/playground/src/components/sidebar/components/ContractSelector.tsx @@ -0,0 +1,176 @@ +import { useState, useEffect, useContext } from 'react'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { type SelectChangeEvent } from '@mui/material/Select'; +import Divider from '@mui/material/Divider'; +import ListSubheader from '@mui/material/ListSubheader'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import { CopyToClipboardButton } from '../../common/CopyToClipboardButton'; +import { + convertFromUTF8BufferAsString, + formatFrAsString, + parseAliasedBuffersAsString, +} from '../../../utils/conversion'; +import { PREDEFINED_CONTRACTS } from '../../../utils/types'; +import { css } from '@emotion/react'; +import { AztecContext } from '../../../aztecEnv'; +import { AztecAddress, loadContractArtifact } from '@aztec/aztec.js'; +import { parse } from 'buffer-json'; +import { select } from '../../../styles/common'; +import { filterDeployedAliasedContracts } from '../../../utils/contracts'; + +const modalContainer = css({ + padding: '10px 0', +}); + +const loadingContainer = css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + padding: '20px 0', + gap: '12px', +}); + +export function ContractSelector() { + const [contracts, setContracts] = useState([]); + + const [isContractsLoading, setIsContractsLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + const [selectedPredefinedContract, setSelectedPredefinedContract] = useState(undefined); + + const { + currentContractAddress, + wallet, + walletDB, + isPXEInitialized, + setCurrentContractArtifact, + setCurrentContractAddress, + setShowContractInterface, + } = useContext(AztecContext); + + useEffect(() => { + const refreshContracts = async () => { + setIsContractsLoading(true); + const aliasedContracts = await walletDB.listAliases('contracts'); + const contracts = parseAliasedBuffersAsString(aliasedContracts); + // Temporarily filter out undeployed contracts + const deployedContracts = await filterDeployedAliasedContracts(contracts, wallet); + setContracts(deployedContracts); + setIsContractsLoading(false); + }; + if (walletDB && wallet) { + refreshContracts(); + } + }, [currentContractAddress, walletDB, wallet]); + + const handleContractChange = async (event: SelectChangeEvent) => { + const contractValue = event.target.value; + if (contractValue === '') { + return; + } + + // If 'upload your own' is selected, set the contract artifact to undefined, and allow the user to upload a new one + if (contractValue === PREDEFINED_CONTRACTS.CUSTOM_UPLOAD) { + setCurrentContractArtifact(undefined); + setCurrentContractAddress(undefined); + setSelectedPredefinedContract(contractValue); + setShowContractInterface(true); + return; + } + + setIsContractsLoading(true); + + try { + if ([PREDEFINED_CONTRACTS.SIMPLE_VOTING, PREDEFINED_CONTRACTS.SIMPLE_TOKEN].includes(contractValue)) { + let contractArtifactJSON; + switch (contractValue) { + case PREDEFINED_CONTRACTS.SIMPLE_VOTING: + ({ EasyPrivateVotingContractArtifact: contractArtifactJSON } = await import( + '@aztec/noir-contracts.js/EasyPrivateVoting' + )); + break; + case PREDEFINED_CONTRACTS.SIMPLE_TOKEN: + ({ SimpleTokenContractArtifact: contractArtifactJSON } = await import( + '@aztec/noir-contracts.js/SimpleToken' + )); + break; + } + const contractArtifact = await loadContractArtifact(contractArtifactJSON); + setSelectedPredefinedContract(contractValue); + setCurrentContractArtifact(contractArtifact); + setCurrentContractAddress(undefined); + setShowContractInterface(true); + } else { + const artifactAsString = await walletDB.retrieveAlias(`artifacts:${contractValue}`); + const contractArtifact = loadContractArtifact(parse(convertFromUTF8BufferAsString(artifactAsString))); + setCurrentContractAddress(AztecAddress.fromString(contractValue)); + setCurrentContractArtifact(contractArtifact); + setSelectedPredefinedContract(undefined); + setShowContractInterface(true); + } + } finally { + setIsContractsLoading(false); + } + }; + + if (!isPXEInitialized || !wallet) { + return ( +
+ + Note: Connect to a network and account to deploy and interact with contracts + +
+ ); + } + + return ( +
+ + Contracts + + {isContractsLoading ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +} diff --git a/playground/src/components/sidebar/components/CreateAccountDialog.tsx b/playground/src/components/sidebar/components/CreateAccountDialog.tsx new file mode 100644 index 000000000000..dbce4b9026aa --- /dev/null +++ b/playground/src/components/sidebar/components/CreateAccountDialog.tsx @@ -0,0 +1,183 @@ +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import { Fr, DeployMethod, type DeployOptions, AccountWallet } from '@aztec/aztec.js'; +import { getSchnorrAccount } from '@aztec/accounts/schnorr/lazy'; +import { getEcdsaRAccount, getEcdsaKAccount } from '@aztec/accounts/ecdsa/lazy'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import { useContext, useState } from 'react'; +import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { AztecContext } from '../../../aztecEnv'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import type { AccountType } from '../../../utils/storage'; +import { randomBytes } from '@aztec/foundation/crypto'; +import { FeePaymentSelector } from '../../common/FeePaymentSelector'; +import CircularProgress from '@mui/material/CircularProgress'; +import InputLabel from '@mui/material/InputLabel'; +import Typography from '@mui/material/Typography'; +import FormGroup from '@mui/material/FormGroup'; +import { progressIndicator, dialogBody, form } from '../../../styles/common'; +import { InfoText } from '../../common/InfoText'; +import { INFO_TEXT } from '../../../constants'; + +export function CreateAccountDialog({ + open, + onClose, +}: { + open: boolean; + onClose: ( + accountWallet?: AccountWallet, + publiclyDeploy?: boolean, + interaction?: DeployMethod, + opts?: DeployOptions, + ) => void; +}) { + const [alias, setAlias] = useState(''); + const [type, setType] = useState('ecdsasecp256r1'); + const [secretKey] = useState(Fr.random()); + const [publiclyDeploy, setPubliclyDeploy] = useState(true); + const [isRegistering, setIsRegistering] = useState(false); + const [error, setError] = useState(null); + + const [feePaymentMethod, setFeePaymentMethod] = useState(null); + + const { pxe, walletDB } = useContext(AztecContext); + + const createAccount = async () => { + setIsRegistering(true); + try { + const salt = Fr.random(); + let accountManager; + let signingKey; + switch (type) { + case 'schnorr': { + signingKey = deriveSigningKey(secretKey); + accountManager = await getSchnorrAccount(pxe, secretKey, signingKey, salt); + break; + } + case 'ecdsasecp256r1': { + signingKey = randomBytes(32); + accountManager = await getEcdsaRAccount(pxe, secretKey, signingKey, salt); + break; + } + case 'ecdsasecp256k1': { + signingKey = randomBytes(32); + accountManager = await getEcdsaKAccount(pxe, secretKey, signingKey, salt); + break; + } + default: { + throw new Error('Unknown account type'); + } + } + const accountWallet = await accountManager.getWallet(); + await accountManager.register(); + await walletDB.storeAccount(accountWallet.getAddress(), { + type, + secretKey: accountWallet.getSecretKey(), + alias, + salt, + signingKey, + }); + await walletDB.storeAccountMetadata(accountWallet.getAddress(), 'deployed', Buffer.from([0])); + let deployMethod: DeployMethod; + let opts: DeployOptions; + if (publiclyDeploy) { + deployMethod = await accountManager.getDeployMethod(); + opts = { + contractAddressSalt: salt, + fee: { + paymentMethod: await accountManager.getSelfPaymentMethod(feePaymentMethod), + }, + universalDeploy: true, + skipClassRegistration: true, + skipPublicDeployment: true, + }; + } + onClose(accountWallet, publiclyDeploy, deployMethod, opts); + } catch (e) { + setError(e.message); + } finally { + setIsRegistering(false); + } + }; + + const handleClose = () => { + onClose(); + }; + + return ( + + Create account +
+ + + Account type + + {INFO_TEXT.ACCOUNT_ABSTRACTION} + + + { + setAlias(event.target.value); + }} + /> + {INFO_TEXT.ALIASES} + + {/* Always deploy for now */} + {/* + setPubliclyDeploy(event.target.checked)} /> + } + label="Deploy" + /> + */} + {publiclyDeploy && } + +
+ {!error ? ( + isRegistering ? ( +
+ + Registering account... + + +
+ ) : ( + + ) + ) : ( + + An error occurred: {error} + + )} + +
+
+ ); +} diff --git a/playground/src/components/sidebar/components/NetworkSelector.tsx b/playground/src/components/sidebar/components/NetworkSelector.tsx new file mode 100644 index 000000000000..2fb39e0ec074 --- /dev/null +++ b/playground/src/components/sidebar/components/NetworkSelector.tsx @@ -0,0 +1,247 @@ +import { useContext, useEffect, useState } from 'react'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { type SelectChangeEvent } from '@mui/material/Select'; +import Typography from '@mui/material/Typography'; +import AddIcon from '@mui/icons-material/Add'; +import { createStore } from '@aztec/kv-store/indexeddb'; +import { AddNetworksDialog } from './AddNetworkDialog'; +import { css } from '@emotion/react'; +import Link from '@mui/material/Link'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import CircularProgress from '@mui/material/CircularProgress'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { AztecContext, AztecEnv, WebLogger } from '../../../aztecEnv'; +import { NetworkDB, WalletDB } from '../../../utils/storage'; +import { parseAliasedBuffersAsString } from '../../../utils/conversion'; +import { select } from '../../../styles/common'; +import { NETWORKS } from '../../../utils/networks'; + +const modalContainer = css({ + padding: '10px 0', +}); + +const errorMessageStyle = css({ + display: 'flex', + alignItems: 'center', + gap: '8px', + padding: '12px', + marginTop: '12px', + backgroundColor: 'rgba(211, 47, 47, 0.1)', + borderRadius: '8px', + color: '#d32f2f', + fontSize: '14px', + lineHeight: '1.4', +}); + +const errorIcon = css({ + fontSize: '20px', +}); + +const loadingContainer = css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '20px 0', + gap: '10px', +}); + +interface NetworkSelectorProps {} + +export function NetworkSelector({}: NetworkSelectorProps) { + const { + setConnecting, + setPXE, + setNetwork, + setPXEInitialized, + setWalletDB, + setAztecNode, + setLogs, + setWallet, + setCurrentContractAddress, + setCurrentContractArtifact, + setShowContractInterface, + network, + connecting, + } = useContext(AztecContext); + + const [networks, setNetworks] = useState(NETWORKS); + const [isNetworkStoreInitialized, setIsNetworkStoreInitialized] = useState(false); + const [openAddNetworksDialog, setOpenAddNetworksDialog] = useState(false); + const [isOpen, setOpen] = useState(false); + + useEffect(() => { + const initNetworkStore = async () => { + await AztecEnv.initNetworkStore(); + setIsNetworkStoreInitialized(true); + }; + initNetworkStore(); + }, []); + + useEffect(() => { + const refreshNetworks = async () => { + const aliasedBuffers = await NetworkDB.getInstance().listNetworks(); + const aliasedNetworks = parseAliasedBuffersAsString(aliasedBuffers); + const networks = [ + ...NETWORKS, + ...aliasedNetworks.map(network => ({ + nodeURL: network.value, + name: network.key, + description: 'Custom network', + hasTestAccounts: false, + hasSponsoredFPC: true, + })), + ]; + setNetworks(networks); + }; + if (isNetworkStoreInitialized) { + refreshNetworks(); + } + }, [isNetworkStoreInitialized]); + + const handleNetworkChange = async (event: SelectChangeEvent) => { + setConnecting(true); + setPXEInitialized(false); + const network = networks.find(network => network.nodeURL === event.target.value); + const node = await AztecEnv.connectToNode(network.nodeURL); + setAztecNode(node); + setNetwork(network); + setWallet(null); + setCurrentContractAddress(null); + setCurrentContractArtifact(null); + setShowContractInterface(false); + const pxe = await AztecEnv.initPXE(node, setLogs); + const rollupAddress = (await pxe.getNodeInfo()).l1ContractAddresses.rollupAddress; + const walletLogger = WebLogger.getInstance().createLogger('wallet:data:idb'); + const walletDBStore = await createStore( + `wallet-${rollupAddress}`, + { dataDirectory: 'wallet', dataStoreMapSizeKB: 2e10 }, + walletLogger, + ); + const walletDB = WalletDB.getInstance(); + walletDB.init(walletDBStore, walletLogger.info); + setPXE(pxe); + setWalletDB(walletDB); + setPXEInitialized(true); + setConnecting(false); + }; + + const handleNetworkAdded = async (network?: string, alias?: string) => { + if (network && alias) { + await NetworkDB.getInstance().storeNetwork(alias, network); + const aliasedBuffers = await NetworkDB.getInstance().listNetworks(); + const aliasedNetworks = parseAliasedBuffersAsString(aliasedBuffers); + const networks = [ + ...NETWORKS, + ...aliasedNetworks.map(network => ({ + nodeURL: network.value, + name: network.key, + description: 'Custom network', + hasTestAccounts: false, + hasSponsoredFPC: true, + })), + ]; + setNetworks(networks); + } + setOpenAddNetworksDialog(false); + }; + + // // Renders the appropriate error message based on network type + // const renderErrorMessage = () => { + // if (isSandboxError) { + // return ( + // <> + // {errorText} + //
Do you have a sandbox running? Check out the{' '} + // + // docs + // + // + // ); + // } else if (isTestnetError) { + // return ( + // <> + // {errorText} + //
+ //
Testnet may be down. Please see our Discord for updates. + // + // ); + // } else { + // return ( + // <> + // {errorText} + //
+ //
Are your network details correct? Please reach out on Discord for help troubleshooting. + // + // ); + // } + // }; + + return ( + <> + {connecting ? ( +
+ + Connecting to network... +
+ ) : ( +
+ + + + + {/* {connectionError && ( +
+ +
{renderErrorMessage()}
+
+ )} */} +
+ )} + + + ); +} diff --git a/playground/src/components/sidebar/components/txsPanel.tsx b/playground/src/components/sidebar/components/TxsPanel.tsx similarity index 60% rename from playground/src/components/sidebar/components/txsPanel.tsx rename to playground/src/components/sidebar/components/TxsPanel.tsx index 27fabb6afd93..a865b9f74e02 100644 --- a/playground/src/components/sidebar/components/txsPanel.tsx +++ b/playground/src/components/sidebar/components/TxsPanel.tsx @@ -3,12 +3,14 @@ import { useContext, useEffect, useState } from 'react'; import { AztecContext } from '../../../aztecEnv'; import Typography from '@mui/material/Typography'; import { convertFromUTF8BufferAsString, formatFrAsString } from '../../../utils/conversion'; -import { type ContractFunctionInteractionTx } from '../../../utils/txs'; +import { type UserTx } from '../../../utils/txs'; import { TxHash } from '@aztec/aztec.js'; +import Divider from '@mui/material/Divider'; const txPanel = css({ width: '100%', backgroundColor: 'var(--mui-palette-primary-main)', + minHeight: '75px', maxHeight: '30vh', overflowY: 'auto', borderRadius: '0.5rem', @@ -33,16 +35,16 @@ export function TxsPanel({ ...props }) { const refreshTransactions = async () => { const txsPerContract = await walletDB.retrieveTxsPerContract(currentContractAddress); const txHashes = txsPerContract.map(txHash => TxHash.fromString(convertFromUTF8BufferAsString(txHash))); - const txs: ContractFunctionInteractionTx[] = await Promise.all( + const txs: UserTx[] = await Promise.all( txHashes.map(async txHash => { const txData = await walletDB.retrieveTxData(txHash); return { contractAddress: currentContractAddress, txHash: txData.txHash, status: convertFromUTF8BufferAsString(txData.status), - fnName: convertFromUTF8BufferAsString(txData.fnName), + name: convertFromUTF8BufferAsString(txData.name), date: parseInt(convertFromUTF8BufferAsString(txData.date)), - } as ContractFunctionInteractionTx; + } as UserTx; }), ); txs.sort((a, b) => (b.date >= a.date ? -1 : 1)); @@ -63,25 +65,33 @@ export function TxsPanel({ ...props }) { }, [currentContractAddress, currentTx]); return ( -
- {transactions.map(tx => ( -
-
- - {tx.txHash ? formatFrAsString(tx.txHash.toString()) : '()'} -  -  - - - {tx.receipt ? tx.receipt.status.toUpperCase() : tx.status.toUpperCase()} -   - {tx.receipt && tx.receipt.status === 'error' ? tx.receipt.error : tx.error} - + <> + {currentContractAddress && ( + <> + Transactions + +
+ {transactions.map(tx => ( +
+
+ + {tx.txHash ? formatFrAsString(tx.txHash.toString()) : '()'} +  -  + + + {tx.receipt ? tx.receipt.status.toUpperCase() : tx.status.toUpperCase()} +   + {tx.receipt && tx.receipt.status === 'error' ? tx.receipt.error : tx.error} + +
+ + {tx.name}@{formatFrAsString(tx.contractAddress.toString())} + +
+ ))}
- - {tx.fnName}@{formatFrAsString(tx.contractAddress.toString())} - -
- ))} -
+ + )} + ); } diff --git a/playground/src/components/sidebar/components/createAccountDialog.tsx b/playground/src/components/sidebar/components/createAccountDialog.tsx deleted file mode 100644 index 3f9dff0a9b62..000000000000 --- a/playground/src/components/sidebar/components/createAccountDialog.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import DialogTitle from '@mui/material/DialogTitle'; -import Dialog from '@mui/material/Dialog'; -import { AccountWalletWithSecretKey, Fr } from '@aztec/aztec.js'; -import { getSchnorrAccount } from '@aztec/accounts/schnorr/lazy'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import CircularProgress from '@mui/material/CircularProgress'; -import Typography from '@mui/material/Typography'; -import { css } from '@mui/styled-engine'; -import { useContext, useState } from 'react'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; -import { AztecContext } from '../../../aztecEnv'; - -const creationForm = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - padding: '1rem', - alignItems: 'center', -}); - -export function CreateAccountDialog({ - open, - onClose, -}: { - open: boolean; - onClose: (account?: AccountWalletWithSecretKey, salt?: Fr, alias?: string) => void; -}) { - const [alias, setAlias] = useState(''); - const [secretKey] = useState(Fr.random()); - const [deployingAccount, setDeployingAccount] = useState(false); - const { pxe, setDrawerOpen, setLogsOpen } = useContext(AztecContext); - - const createAccount = async () => { - setDeployingAccount(true); - setDrawerOpen(false); - setLogsOpen(true); - const salt = Fr.random(); - const account = await getSchnorrAccount(pxe, secretKey, deriveSigningKey(secretKey), salt); - await account.deploy().wait(); - const wallet = await account.getWallet(); - setDeployingAccount(false); - onClose(wallet, salt, alias); - }; - - const handleClose = () => { - onClose(); - }; - - return ( - - Create account -
- {deployingAccount ? ( - <> - Deploying... - - - ) : ( - <> - { - setAlias(event.target.value); - }} - /> - - - - )} -
-
- ); -} diff --git a/playground/src/components/sidebar/sidebar.tsx b/playground/src/components/sidebar/sidebar.tsx deleted file mode 100644 index bc2917e3483b..000000000000 --- a/playground/src/components/sidebar/sidebar.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { css } from '@mui/styled-engine'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import Select, { type SelectChangeEvent } from '@mui/material/Select'; -import { AztecEnv, AztecContext, WebLogger } from '../../aztecEnv'; -import { createStore } from '@aztec/kv-store/indexeddb'; -import { AccountWalletWithSecretKey, Fr, AztecAddress, AccountManager } from '@aztec/aztec.js'; -import { getInitialTestAccounts } from '@aztec/accounts/testing/lazy'; -import { NetworkDB, WalletDB } from '../../utils/storage'; -import { useContext, useEffect, useState } from 'react'; -import { CreateAccountDialog } from './components/createAccountDialog'; -import { getSchnorrAccount } from '@aztec/accounts/schnorr/lazy'; -import AddIcon from '@mui/icons-material/Add'; -import Button from '@mui/material/Button'; -import Divider from '@mui/material/Divider'; -import Typography from '@mui/material/Typography'; -import { formatFrAsString, parseAliasedBuffersAsString } from '../../utils/conversion'; -import ContactsIcon from '@mui/icons-material/Contacts'; -import { CopyToClipboardButton } from '../common/copyToClipboardButton'; -import { AddSendersDialog } from './components/addSenderDialog'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; -import { TxsPanel } from './components/txsPanel'; -import { AddNetworksDialog } from './components/addNetworkDialog'; - -const container = css({ - display: 'flex', - flexDirection: 'column', - height: '100%', - width: '100%', - backgroundColor: 'var(--mui-palette-primary-light)', - overflow: 'hidden', - padding: '0 0.5rem', - textAlign: 'center', -}); - -const select = css({ - display: 'flex', - flexDirection: 'row', - width: '100%', - margin: '0.5rem 0rem', -}); - -const header = css({ - display: 'flex', - flexDirection: 'row', - height: '5rem', - width: '100%', - alignItems: 'center', - marginBottom: '1rem', -}); - -type Network = { nodeURL: string; name: string }; - -const NETWORKS: Network[] = [ - { - nodeURL: 'http://localhost:8080', - name: 'Local', - }, -]; - -export function SidebarComponent() { - const { - setPXE, - setNodeURL, - setPXEInitialized, - setWalletDB, - setWallet, - setCurrentContractAddress, - setAztecNode, - setLogs, - currentContractAddress, - wallet, - walletDB, - nodeURL, - isPXEInitialized, - pxe, - } = useContext(AztecContext); - const [changingNetworks, setChangingNetworks] = useState(false); - const [accounts, setAccounts] = useState([]); - const [contracts, setContracts] = useState([]); - const [networks, setNetworks] = useState(NETWORKS); - const [openAddNetworksDialog, setOpenAddNetworksDialog] = useState(false); - const [openCreateAccountDialog, setOpenCreateAccountDialog] = useState(false); - const [openAddSendersDialog, setOpenAddSendersDialog] = useState(false); - - const getAccountsAndSenders = async () => { - const aliasedBuffers = await walletDB.listAliases('accounts'); - const aliasedAccounts = parseAliasedBuffersAsString(aliasedBuffers); - const testAccountData = await getInitialTestAccounts(); - let i = 0; - for (const accountData of testAccountData) { - const account: AccountManager = await getSchnorrAccount( - pxe, - accountData.secret, - accountData.signingKey, - accountData.salt, - ); - if (!aliasedAccounts.find(({ value }) => account.getAddress().equals(AztecAddress.fromString(value)))) { - await account.register(); - const instance = account.getInstance(); - const wallet = await account.getWallet(); - const alias = `test${i}`; - await walletDB.storeAccount(instance.address, { - type: 'schnorr', - secretKey: wallet.getSecretKey(), - alias, - salt: account.getInstance().salt, - }); - aliasedAccounts.push({ - key: `accounts:${alias}`, - value: instance.address.toString(), - }); - } - i++; - } - const pxeAccounts = await pxe.getRegisteredAccounts(); - const ourAccounts = []; - const senders = []; - aliasedAccounts.forEach(({ key, value }) => { - if (pxeAccounts.find(account => account.address.equals(AztecAddress.fromString(value)))) { - ourAccounts.push({ key, value }); - } else { - senders.push(key, value); - } - }); - return { ourAccounts, senders }; - }; - - useEffect(() => { - const refreshNetworks = async () => { - const aliasedBuffers = await NetworkDB.getInstance().listNetworks(); - const aliasedNetworks = parseAliasedBuffersAsString(aliasedBuffers); - const networks = [ - ...NETWORKS, - ...aliasedNetworks.map(network => ({ - nodeURL: network.value, - name: network.key, - })), - ]; - setNetworks(networks); - }; - refreshNetworks(); - }, []); - - const handleNetworkChange = async (event: SelectChangeEvent) => { - setChangingNetworks(true); - setPXEInitialized(false); - const nodeURL = event.target.value; - setNodeURL(nodeURL); - const node = await AztecEnv.connectToNode(nodeURL); - setAztecNode(node); - const pxe = await AztecEnv.initPXE(node, setLogs); - const rollupAddress = (await pxe.getNodeInfo()).l1ContractAddresses.rollupAddress; - const walletLogger = WebLogger.getInstance().createLogger('wallet:data:idb'); - const walletDBStore = await createStore( - `wallet-${rollupAddress}`, - { dataDirectory: 'wallet', dataStoreMapSizeKB: 2e10 }, - walletLogger, - ); - const walletDB = WalletDB.getInstance(); - walletDB.init(walletDBStore, walletLogger.info); - setPXE(pxe); - setWalletDB(walletDB); - setPXEInitialized(true); - setChangingNetworks(false); - }; - - useEffect(() => { - const refreshContracts = async () => { - const aliasedContracts = await walletDB.listAliases('contracts'); - setContracts(parseAliasedBuffersAsString(aliasedContracts)); - }; - if (walletDB) { - refreshContracts(); - } - }, [currentContractAddress, walletDB]); - - useEffect(() => { - const refreshAccounts = async () => { - const { ourAccounts } = await getAccountsAndSenders(); - setAccounts(ourAccounts); - }; - if (walletDB && walletDB && pxe) { - refreshAccounts(); - } - }, [wallet, walletDB, pxe]); - - const handleAccountChange = async (event: SelectChangeEvent) => { - if (event.target.value == '') { - return; - } - const accountAddress = AztecAddress.fromString(event.target.value); - const accountData = await walletDB.retrieveAccount(accountAddress); - const account = await getSchnorrAccount( - pxe, - accountData.secretKey, - deriveSigningKey(accountData.secretKey), - accountData.salt, - ); - setWallet(await account.getWallet()); - }; - - const handleAccountCreation = async (account?: AccountWalletWithSecretKey, salt?: Fr, alias?: string) => { - if (account && salt && alias) { - await walletDB.storeAccount(account.getAddress(), { - type: 'schnorr', - secretKey: account.getSecretKey(), - alias, - salt, - }); - const aliasedAccounts = await walletDB.listAliases('accounts'); - setAccounts(parseAliasedBuffersAsString(aliasedAccounts)); - setWallet(account); - } - - setOpenCreateAccountDialog(false); - }; - - const handleContractChange = async (event: SelectChangeEvent) => { - if (event.target.value == '') { - return; - } - const contractAddress = AztecAddress.fromString(event.target.value); - setCurrentContractAddress(contractAddress); - }; - - const handleSenderAdded = async (sender?: AztecAddress, alias?: string) => { - if (sender && alias) { - await wallet.registerSender(sender); - await walletDB.storeAlias('accounts', alias, Buffer.from(sender.toString())); - const { ourAccounts } = await getAccountsAndSenders(); - setAccounts(ourAccounts); - } - setOpenAddSendersDialog(false); - }; - - const handleNetworkAdded = async (network?: string, alias?: string) => { - if (network && alias) { - await NetworkDB.getInstance().storeNetwork(alias, network); - const aliasedBuffers = await NetworkDB.getInstance().listNetworks(); - const aliasedNetworks = parseAliasedBuffersAsString(aliasedBuffers); - const networks = [ - ...NETWORKS, - ...aliasedNetworks.map(network => ({ - nodeURL: network.value, - name: network.key, - })), - ]; - setNetworks(networks); - } - setOpenAddNetworksDialog(false); - }; - - return ( -
-
- - Playground - -
- Connect - - Network - - - - {pxe && isPXEInitialized ? ( - <> - - Account - - - - - ) : ( - <> - )} - {wallet && ( - <> - Tools - - Contracts - - - - - - - )} -
- Transactions - - - -
- ); -} diff --git a/playground/src/constants.ts b/playground/src/constants.ts new file mode 100644 index 000000000000..dd90bfcbecf5 --- /dev/null +++ b/playground/src/constants.ts @@ -0,0 +1,72 @@ +export const PREDEFINED_CONTRACTS = { + SIMPLE_VOTING: 'simple_voting', + SIMPLE_TOKEN: 'simple_token', + CUSTOM_UPLOAD: 'custom_upload', +}; + +export const FORBIDDEN_FUNCTIONS = ['process_log', 'sync_notes', 'public_dispatch']; + +export const TOKEN_ALLOWED_FUNCTIONS = [ + 'mint_privately', + 'mint_publicly', + 'private_transfer', + 'public_transfer', + 'transfer_from_private_to_public', + 'transfer_from_public_to_private', + 'name', + 'symbol', + 'decimals', + 'public_get_name', + 'public_get_symbol', + 'public_get_decimals', + 'public_total_supply', + 'public_balance_of', + 'private_balance_of', + 'burn_public', + 'burn_private', + 'prepare_private_balance_increase', + 'finalize_transfer_to_private', + 'finalize_mint_to_private', + 'cancel_authwit', +]; + +export const FUNCTION_DESCRIPTIONS = { + // SimpleVoting functions + constructor: 'Initialize the voting contract with an admin who can end the vote', + cast_vote: 'Cast a private vote for a candidate without revealing who you voted for', + end_vote: 'End the voting process and prevent further vote submissions', + get_vote: 'View the total number of votes for a specific candidate', + + // SimpleToken functions + mint_privately: 'Create new tokens privately for a specified address', + mint_publicly: 'Create new tokens publicly for a specified address', + private_transfer: 'Transfer tokens without revealing the amount or participants', + public_transfer: 'Transfer tokens publicly where amounts and participants are visible to everyone', + transfer_from_private_to_public: 'Move tokens from private to public state, revealing them on-chain', + transfer_from_public_to_private: 'Move tokens from public to private state, hiding them from public view', + name: 'Get the name of the token', + symbol: "Get the token's ticker symbol", + decimals: 'Get the number of decimal places supported by the token', + public_get_name: 'Get the token name from a public function', + public_get_symbol: 'Get the token symbol from a public function', + public_get_decimals: 'Get the token decimals from a public function', + public_total_supply: 'View the total number of tokens in circulation', + public_balance_of: 'View the public token balance of a specific address', + private_balance_of: 'View the private token balance of a specific address', + burn_public: 'Destroy tokens from a public balance, reducing total supply', + burn_private: 'Destroy tokens from a private balance, reducing total supply', + prepare_private_balance_increase: 'Prepare for a private balance increase operation', + finalize_transfer_to_private: 'Complete a previously initiated transfer to private state', + finalize_mint_to_private: 'Complete a previously initiated private mint operation', + cancel_authwit: 'Cancel a previously created authorization witness', +}; + +export const INFO_TEXT = { + ACCOUNT_ABSTRACTION: + 'Aztec has native Account Abstraction, you can choose the type of signature you want for your account contract', + ALIASES: "Give friendly names to objects you interact with, so they're easier to find later", + FEE_ABSTRACTION: + 'Aztec has native Fee Abstraction, so you can choose to pay fees in multiple ways. For test networks, we have you covered! Use our Sponsored FPC (Fee Paying Contract) and your transactions are on us', + AUTHWITS: + 'Authorization witnesses (AuthWits) allow execution of functions on behalf of other addresses you own (you have their secret keys)', +}; diff --git a/playground/src/hooks/useTransaction.tsx b/playground/src/hooks/useTransaction.tsx new file mode 100644 index 000000000000..98cf46d3b0ac --- /dev/null +++ b/playground/src/hooks/useTransaction.tsx @@ -0,0 +1,66 @@ +import { useContext, useState } from 'react'; +import { AztecContext } from '../aztecEnv'; +import { + TxStatus, + type AztecAddress, + type ContractFunctionInteraction, + type DeployMethod, + type DeployOptions, + type SendMethodOptions, +} from '@aztec/aztec.js'; + +export function useTransaction() { + const { walletDB, currentTx, setCurrentTx } = useContext(AztecContext); + + async function sendTx( + name: string, + interaction: ContractFunctionInteraction | DeployMethod, + contractAddress: AztecAddress, + opts: SendMethodOptions | DeployOptions, + ): Promise { + let receipt; + let txHash; + const tx = { + status: 'proving' as const, + name, + contractAddress, + }; + setCurrentTx(tx); + try { + const provenInteraction = await interaction.prove(opts); + txHash = await provenInteraction.getTxHash(); + setCurrentTx({ + ...currentTx, + ...{ txHash, status: 'sending' }, + }); + receipt = await provenInteraction.send().wait({ dontThrowOnRevert: true }); + await walletDB.storeTx({ + contractAddress, + txHash, + name, + receipt, + }); + setCurrentTx({ + ...currentTx, + ...{ + txHash, + status: receipt.status, + receipt, + error: receipt.error, + }, + }); + } catch (e) { + setCurrentTx({ + ...currentTx, + ...{ + txHash, + status: 'error', + error: e.message, + }, + }); + } + return receipt && receipt.status === TxStatus.SUCCESS; + } + + return { sendTx }; +} diff --git a/playground/src/styles/common.tsx b/playground/src/styles/common.tsx new file mode 100644 index 000000000000..173764e74a17 --- /dev/null +++ b/playground/src/styles/common.tsx @@ -0,0 +1,39 @@ +import { css } from '@mui/styled-engine'; + +export const dialogBody = css({ + display: 'flex', + flexDirection: 'column', + padding: '1rem', + alignItems: 'center', + minWidth: '350px', + minHeight: '500px', +}); + +export const form = css({ + width: '100%', + display: 'flex', + gap: '1rem', +}); + +export const progressIndicator = css({ + display: 'flex', + alignItems: 'center', + height: '20px', + marginBottom: '0.5rem', +}); + +export const select = css({ + display: 'flex', + flexDirection: 'row', + width: '100%', + margin: '0.5rem 0rem', + '@media (max-width: 400px)': { + flexDirection: 'column', + }, +}); + +export const dropdownIcon = css({ + fontSize: '20px', + marginLeft: '8px', + transition: 'transform 0.3s ease', +}); diff --git a/playground/src/utils/contracts.ts b/playground/src/utils/contracts.ts new file mode 100644 index 000000000000..9f042fcec248 --- /dev/null +++ b/playground/src/utils/contracts.ts @@ -0,0 +1,18 @@ +import { AztecAddress, type PXE, type Wallet } from '@aztec/aztec.js'; + +export async function filterDeployedAliasedContracts( + aliasedContracts: { key: string; value: string }[], + walletOrPxe: PXE | Wallet, +) { + const deployed = ( + await Promise.all( + aliasedContracts.map(async alias => { + const { isContractPubliclyDeployed } = await walletOrPxe.getContractMetadata( + AztecAddress.fromString(alias.value), + ); + return { ...alias, deployed: isContractPubliclyDeployed }; + }), + ) + ).filter(contract => contract.deployed); + return deployed; +} diff --git a/playground/src/utils/networks.ts b/playground/src/utils/networks.ts new file mode 100644 index 000000000000..af1728c39aac --- /dev/null +++ b/playground/src/utils/networks.ts @@ -0,0 +1,24 @@ +export type Network = { + nodeURL: string; + name: string; + description: string; + hasTestAccounts: boolean; + hasSponsoredFPC: boolean; +}; + +export const NETWORKS: Network[] = [ + { + nodeURL: 'http://104.198.9.75:8080', + name: 'Aztec Devnet', + description: 'Public development network', + hasTestAccounts: false, + hasSponsoredFPC: true, + }, + { + nodeURL: 'http://localhost:8080', + name: 'Local Sandbox', + description: 'Run your own sandbox', + hasTestAccounts: true, + hasSponsoredFPC: true, + }, +]; diff --git a/playground/src/utils/sponsoredFPC.ts b/playground/src/utils/sponsoredFPC.ts new file mode 100644 index 000000000000..de5b4d43d7c6 --- /dev/null +++ b/playground/src/utils/sponsoredFPC.ts @@ -0,0 +1,34 @@ +import { + type ContractInstanceWithAddress, + type PXE, + getContractInstanceFromDeployParams, + SponsoredFeePaymentMethod, + AztecAddress, + Fr, +} from '@aztec/aztec.js'; +import { SPONSORED_FPC_SALT } from '@aztec/constants'; +import { SponsoredFPCContract } from '@aztec/noir-contracts.js/SponsoredFPC'; + +export async function getSponsoredFPCInstance(): Promise { + return await getContractInstanceFromDeployParams(SponsoredFPCContract.artifact, { + salt: new Fr(SPONSORED_FPC_SALT), + }); +} + +export async function getSponsoredFPCAddress(): Promise { + return (await getSponsoredFPCInstance()).address; +} + +export async function prepareForFeePayment(pxe: PXE): Promise { + try { + const sponsoredFPC = await getSponsoredFPCInstance(); + await pxe.registerContract({ + instance: sponsoredFPC, + artifact: SponsoredFPCContract.artifact, + }); + return new SponsoredFeePaymentMethod(sponsoredFPC.address); + } catch (error) { + console.error('Error preparing SponsoredFeePaymentMethod:', error); + throw error; + } +} diff --git a/playground/src/utils/storage.ts b/playground/src/utils/storage.ts index 628470574e3d..6c12141d262d 100644 --- a/playground/src/utils/storage.ts +++ b/playground/src/utils/storage.ts @@ -1,19 +1,21 @@ -import { - type ContractArtifact, - type AztecAddress, - Fr, - TxReceipt, - type AuthWitness, - type TxHash, -} from '@aztec/aztec.js'; +import { type ContractArtifact, AztecAddress, Fr, TxReceipt, type AuthWitness, type TxHash, Fq } from '@aztec/aztec.js'; import { type LogFn } from '@aztec/foundation/log'; import { type AztecAsyncMap, type AztecAsyncKVStore, type AztecAsyncMultiMap } from '@aztec/kv-store'; import { stringify } from 'buffer-json'; - -export const Aliases = ['accounts', 'contracts', 'artifacts', 'secrets', 'transactions', 'authwits'] as const; +import { parseAliasedBuffersAsString } from './conversion'; + +export const Aliases = [ + 'accounts', + 'contracts', + 'artifacts', + 'secrets', + 'transactions', + 'authwits', + 'senders', +] as const; export type AliasType = (typeof Aliases)[number]; -export const AccountTypes = ['schnorr', 'ecdsasecp256r1ssh', 'ecdsasecp256k1'] as const; +export const AccountTypes = ['schnorr', 'ecdsasecp256r1', 'ecdsasecp256k1'] as const; export type AccountType = (typeof AccountTypes)[number]; export class WalletDB { @@ -85,10 +87,12 @@ export class WalletDB { secretKey, salt, alias, + signingKey, }: { type: AccountType; secretKey: Fr; salt: Fr; + signingKey: Fq | Buffer; alias: string | undefined; }, log: LogFn = this.#userLog, @@ -99,6 +103,10 @@ export class WalletDB { await this.#accounts.set(`${address.toString()}:type`, Buffer.from(type)); await this.#accounts.set(`${address.toString()}:sk`, secretKey.toBuffer()); await this.#accounts.set(`${address.toString()}:salt`, salt.toBuffer()); + await this.#accounts.set( + `${address.toString()}:signingKey`, + 'toBuffer' in signingKey ? signingKey.toBuffer() : signingKey, + ); log(`Account stored in database with alias${alias ? `es last & ${alias}` : ' last'}`); } @@ -127,12 +135,12 @@ export class WalletDB { { contractAddress, txHash, - fnName, + name, receipt, }: { contractAddress: AztecAddress; txHash: TxHash; - fnName: string; + name: string; receipt: TxReceipt; }, log: LogFn = this.#userLog, @@ -143,7 +151,7 @@ export class WalletDB { } await this.#transactionsPerContract.set(`${contractAddress.toString()}`, Buffer.from(txHash.toString())); - await this.#transactions.set(`${txHash.toString()}:fnName`, Buffer.from(fnName)); + await this.#transactions.set(`${txHash.toString()}:name`, Buffer.from(name)); await this.#transactions.set(`${txHash.toString()}:status`, Buffer.from(receipt.status.toString())); await this.#transactions.set(`${txHash.toString()}:date`, Buffer.from(Date.now().toString())); log(`Transaction hash stored in database with alias${alias ? `es last & ${alias}` : ' last'}`); @@ -158,20 +166,20 @@ export class WalletDB { } async retrieveTxData(txHash: TxHash) { - const fnNameBuffer = await this.#transactions.getAsync(`${txHash.toString()}:fnName`); - if (!fnNameBuffer) { + const nameBuffer = await this.#transactions.getAsync(`${txHash.toString()}:name`); + if (!nameBuffer) { throw new Error( - `Could not find ${txHash.toString()}:fnName. Transaction with hash "${txHash.toString()}" does not exist on this wallet.`, + `Could not find ${txHash.toString()}:name. Transaction with hash "${txHash.toString()}" does not exist on this wallet.`, ); } - const fnName = fnNameBuffer.toString(); + const name = nameBuffer.toString(); const status = (await this.#transactions.getAsync(`${txHash.toString()}:status`))!.toString(); const date = await this.#transactions.getAsync(`${txHash.toString()}:date`)!.toString(); return { txHash, - fnName, + name, status, date, }; @@ -233,13 +241,25 @@ export class WalletDB { const secretKey = Fr.fromBuffer(secretKeyBuffer); const salt = Fr.fromBuffer(await this.#accounts.getAsync(`${address.toString()}:salt`)!); const type = (await this.#accounts.getAsync(`${address.toString()}:type`)!).toString('utf8') as AccountType; - return { address, secretKey, salt, type }; + const signingKey = await this.#accounts.getAsync(`${address.toString()}:signingKey`)!; + return { address, secretKey, salt, type, signingKey }; } async storeAlias(type: AliasType, key: string, value: Buffer, log: LogFn = this.#userLog) { await this.#aliases.set(`${type}:${key}`, value); log(`Data stored in database with alias ${type}:${key}`); } + + async deleteAccount(address: AztecAddress) { + await this.#accounts.delete(`${address.toString()}:sk`); + await this.#accounts.delete(`${address.toString()}:salt`); + await this.#accounts.delete(`${address.toString()}:type`); + await this.#accounts.delete(`${address.toString()}:signingKey`); + const aliasesBuffers = await this.listAliases('accounts'); + const aliases = parseAliasedBuffersAsString(aliasesBuffers); + const alias = aliases.find(alias => address.equals(AztecAddress.fromString(alias.value))); + await this.#aliases.delete(`accounts:${alias?.key}`); + } } export class NetworkDB { diff --git a/playground/src/utils/txs.ts b/playground/src/utils/txs.ts index 9279d9cba69b..437ba68fff0e 100644 --- a/playground/src/utils/txs.ts +++ b/playground/src/utils/txs.ts @@ -1,11 +1,11 @@ import { TxHash, TxReceipt, TxStatus, AztecAddress } from '@aztec/aztec.js'; -export type ContractFunctionInteractionTx = { +export type UserTx = { txHash?: TxHash; receipt?: TxReceipt; date?: number; status: 'error' | 'simulating' | 'proving' | 'sending' | TxStatus; - fnName: string; + name: string; error?: string; contractAddress: AztecAddress; }; diff --git a/playground/src/utils/types.ts b/playground/src/utils/types.ts new file mode 100644 index 000000000000..7436ec383adb --- /dev/null +++ b/playground/src/utils/types.ts @@ -0,0 +1,10 @@ +export const PREDEFINED_CONTRACTS = { + SIMPLE_VOTING: 'SimpleVoting', + SIMPLE_TOKEN: 'SimpleToken', + CUSTOM_UPLOAD: 'custom_upload', +}; + +export type AliasedItem = { + key: string; + value: string; +}; diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 25906edaeb52..fd27e37e27d9 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -25,7 +25,7 @@ const nodePolyfillsFix = (options?: PolyfillOptions | undefined): Plugin => { export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { - logLevel: 'error', + logLevel: process.env.CI ? 'error' : undefined, server: { // Headers needed for bb WASM to work in multithreaded mode headers: { diff --git a/playground/yarn.lock b/playground/yarn.lock index af2050e15594..c7ddd50df1ba 100644 --- a/playground/yarn.lock +++ b/playground/yarn.lock @@ -17,6 +17,12 @@ __metadata: languageName: node linkType: soft +"@aztec/constants@link:../yarn-project/constants::locator=%40aztec%2Fplayground%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/constants@link:../yarn-project/constants::locator=%40aztec%2Fplayground%40workspace%3A." + languageName: node + linkType: soft + "@aztec/foundation@link:../yarn-project/foundation::locator=%40aztec%2Fplayground%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec/foundation@link:../yarn-project/foundation::locator=%40aztec%2Fplayground%40workspace%3A." @@ -29,20 +35,30 @@ __metadata: languageName: node linkType: soft +"@aztec/noir-contracts.js@link:../yarn-project/noir-contracts.js::locator=%40aztec%2Fplayground%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/noir-contracts.js@link:../yarn-project/noir-contracts.js::locator=%40aztec%2Fplayground%40workspace%3A." + languageName: node + linkType: soft + "@aztec/playground@workspace:.": version: 0.0.0-use.local resolution: "@aztec/playground@workspace:." dependencies: "@aztec/accounts": "link:../yarn-project/accounts" "@aztec/aztec.js": "link:../yarn-project/aztec.js" + "@aztec/constants": "link:../yarn-project/constants" "@aztec/foundation": "link:../yarn-project/foundation" "@aztec/kv-store": "link:../yarn-project/kv-store" + "@aztec/noir-contracts.js": "link:../yarn-project/noir-contracts.js" "@aztec/pxe": "link:../yarn-project/pxe" "@aztec/stdlib": "link:../yarn-project/stdlib" "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.0" "@eslint/js": "npm:^9.18.0" + "@fontsource/inter": "npm:^5.2.5" "@fontsource/roboto": "npm:^5.1.1" + "@fontsource/space-grotesk": "npm:^5.2.6" "@mui/icons-material": "npm:^6.3.1" "@mui/material": "npm:^6.3.1" "@mui/styles": "npm:^6.3.1" @@ -1108,6 +1124,13 @@ __metadata: languageName: node linkType: hard +"@fontsource/inter@npm:^5.2.5": + version: 5.2.5 + resolution: "@fontsource/inter@npm:5.2.5" + checksum: 10c0/071e12a6a9c8cf6e95da43ba532e80eb8e35c1ffd2384c9c6555d7f222e306e22968f7c4fff5065b9f81503db622cf3de8a688fbc1cb0c04aa3803c797f6f6a0 + languageName: node + linkType: hard + "@fontsource/roboto@npm:^5.1.1": version: 5.1.1 resolution: "@fontsource/roboto@npm:5.1.1" @@ -1115,6 +1138,13 @@ __metadata: languageName: node linkType: hard +"@fontsource/space-grotesk@npm:^5.2.6": + version: 5.2.6 + resolution: "@fontsource/space-grotesk@npm:5.2.6" + checksum: 10c0/4ce284cac461031721c104844dbbeb8df75e3a1d3fd0573fcefc442f3aa8f5dd4362595cd067f09a2a47b7fd9a6f94f6882647da67139fd8e690cff9e603d39f + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts index b686a09116e0..9330330b7404 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts @@ -28,6 +28,7 @@ export async function createPXEService( const configWithContracts = { ...config, l1Contracts, + l2BlockBatchSize: 200, } as PXEServiceConfig; const store = await createStore( diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts index bd5877e25177..44a1a1f2527c 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts @@ -26,6 +26,7 @@ export async function createPXEService( const l1Contracts = await aztecNode.getL1ContractAddresses(); const configWithContracts = { ...config, + l2BlockBatchSize: 200, l1Contracts, } as PXEServiceConfig; diff --git a/yarn-project/pxe/src/entrypoints/server/utils.ts b/yarn-project/pxe/src/entrypoints/server/utils.ts index 84f1672cca9e..819f6034249b 100644 --- a/yarn-project/pxe/src/entrypoints/server/utils.ts +++ b/yarn-project/pxe/src/entrypoints/server/utils.ts @@ -52,6 +52,7 @@ export async function createPXEServiceWithSimulationProvider( const configWithContracts = { ...config, l1Contracts, + l2BlockBatchSize: 200, } as PXEServiceConfig; const store = await createStore(