diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr index 6490b85ff1a3..661b1a1516dd 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr @@ -16,7 +16,7 @@ contract DocsExample { use dep::aztec::prelude::{ AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, NoteViewerOptions, PrivateContext, Map, PublicMutable, PublicImmutable, PrivateMutable, PrivateImmutable, - PrivateSet, SharedImmutable, Storable + PrivateSet, SharedImmutable }; use dep::aztec::{note::note_getter_options::Comparator, context::{PublicContext, Context}}; // how to import methods from other files/folders within your workspace diff --git a/noir/noir-repo/aztec_macros/src/transforms/storage.rs b/noir/noir-repo/aztec_macros/src/transforms/storage.rs index 93ed1c7c96af..bb941a2dc7a3 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/storage.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/storage.rs @@ -266,9 +266,8 @@ pub fn assign_storage_slots( ) -> Result<(), (AztecMacroError, FileId)> { let traits: Vec<_> = collect_traits(context); if let Some((_, file_id)) = get_contract_module_data(context, crate_id) { - let storage_struct = collect_crate_structs(crate_id, context) - .iter() - .find_map(|&struct_id| { + let maybe_storage_struct = + collect_crate_structs(crate_id, context).iter().find_map(|&struct_id| { let r#struct = context.def_interner.get_struct(struct_id); let attributes = context.def_interner.struct_attributes(&struct_id); if attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(storage)")) @@ -278,19 +277,10 @@ pub fn assign_storage_slots( } else { None } - }) - .ok_or(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some("Storage struct not found".to_string()), - }, - file_id, - ))?; + }); - let storage_layout = context - .def_interner - .get_all_globals() - .iter() - .find_map(|global_info| { + let maybe_storage_layout = + context.def_interner.get_all_globals().iter().find_map(|global_info| { let statement = context.def_interner.get_global_let_statement(global_info.id); if statement.clone().is_some_and(|stmt| { stmt.attributes @@ -307,43 +297,44 @@ pub fn assign_storage_slots( } else { None } - }) - .ok_or(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some("Storage layout struct not found".to_string()), - }, - file_id, - ))?; + }); - let init_id = context - .def_interner - .lookup_method( - &Type::Struct(context.def_interner.get_struct(storage_struct.borrow().id), vec![]), - storage_struct.borrow().id, - "init", - false, - ) - .ok_or(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some( - "Storage struct must have an init function".to_string(), + if let (Some(storage_struct), Some(storage_layout)) = + (maybe_storage_struct, maybe_storage_layout) + { + let init_id = context + .def_interner + .lookup_method( + &Type::Struct( + context.def_interner.get_struct(storage_struct.borrow().id), + vec![], ), + storage_struct.borrow().id, + "init", + false, + ) + .ok_or(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some( + "Storage struct must have an init function".to_string(), + ), + }, + file_id, + ))?; + let init_function = + context.def_interner.function(&init_id).block(&context.def_interner); + let init_function_statement_id = init_function.statements().first().ok_or(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some("Init storage statement not found".to_string()), }, file_id, ))?; - let init_function = context.def_interner.function(&init_id).block(&context.def_interner); - let init_function_statement_id = init_function.statements().first().ok_or(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some("Init storage statement not found".to_string()), - }, - file_id, - ))?; - let storage_constructor_statement = - context.def_interner.statement(init_function_statement_id); - - let storage_constructor_expression = match storage_constructor_statement { - HirStatement::Expression(expression_id) => { - match context.def_interner.expression(&expression_id) { + let storage_constructor_statement = + context.def_interner.statement(init_function_statement_id); + + let storage_constructor_expression = match storage_constructor_statement { + HirStatement::Expression(expression_id) => { + match context.def_interner.expression(&expression_id) { HirExpression::Constructor(hir_constructor_expression) => { Ok(hir_constructor_expression) } @@ -357,106 +348,107 @@ pub fn assign_storage_slots( file_id, )), } - } - _ => Err(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some( - "Storage constructor statement must be an expression".to_string(), - ), - }, - file_id, - )), - }?; - - let mut storage_slot: u64 = 1; - for (index, (_, expr_id)) in storage_constructor_expression.fields.iter().enumerate() { - let fields = storage_struct.borrow().get_fields(&[]); - let (field_name, field_type) = fields.get(index).unwrap(); - let new_call_expression = match context.def_interner.expression(expr_id) { - HirExpression::Call(hir_call_expression) => Ok(hir_call_expression), + } _ => Err(( AztecMacroError::CouldNotAssignStorageSlots { secondary_message: Some( - "Storage field initialization expression is not a call expression" - .to_string(), + "Storage constructor statement must be an expression".to_string(), ), }, file_id, )), }?; - let slot_arg_expression = - context.def_interner.expression(&new_call_expression.arguments[1]); + let mut storage_slot: u64 = 1; + for (index, (_, expr_id)) in storage_constructor_expression.fields.iter().enumerate() { + let fields = storage_struct.borrow().get_fields(&[]); + let (field_name, field_type) = fields.get(index).unwrap(); + let new_call_expression = match context.def_interner.expression(expr_id) { + HirExpression::Call(hir_call_expression) => Ok(hir_call_expression), + _ => Err(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some( + "Storage field initialization expression is not a call expression" + .to_string(), + ), + }, + file_id, + )), + }?; + + let slot_arg_expression = + context.def_interner.expression(&new_call_expression.arguments[1]); - let current_storage_slot = match slot_arg_expression { - HirExpression::Literal(HirLiteral::Integer(slot, _)) => Ok(slot.to_u128()), - _ => Err(( + let current_storage_slot = match slot_arg_expression { + HirExpression::Literal(HirLiteral::Integer(slot, _)) => Ok(slot.to_u128()), + _ => Err(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some( + "Storage slot argument expression must be a literal integer" + .to_string(), + ), + }, + file_id, + )), + }?; + + let storage_layout_field = + storage_layout.fields.iter().find(|field| field.0 .0.contents == *field_name); + + let storage_layout_slot_expr = if let Some((_, expr_id)) = storage_layout_field { + let expr = context.def_interner.expression(expr_id); + if let HirExpression::Constructor(storage_layout_field_storable_expr) = expr { + storage_layout_field_storable_expr.fields.iter().find_map( + |(field, expr_id)| { + if field.0.contents == "slot" { + Some(*expr_id) + } else { + None + } + }, + ) + } else { + None + } + } else { + None + } + .ok_or(( AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some( - "Storage slot argument expression must be a literal integer" - .to_string(), - ), + secondary_message: Some(format!( + "Storage layout field ({}) not found or has an incorrect type", + field_name + )), }, file_id, - )), - }?; + ))?; - let storage_layout_field = storage_layout - .fields - .iter() - .find(|field| field.0 .0.contents == field_name.to_string()); - - let storage_layout_slot_expr = if let Some((_, expr_id)) = storage_layout_field { - let expr = context.def_interner.expression(expr_id); - if let HirExpression::Constructor(storage_layout_field_storable_expr) = expr { - storage_layout_field_storable_expr.fields.iter().find_map(|(field, expr_id)| { - if field.0.contents == "slot" { - Some(*expr_id) - } else { - None - } - }) + let new_storage_slot = if current_storage_slot == 0 { + u128::from(storage_slot) } else { - None - } - } else { - None + current_storage_slot + }; + + let type_serialized_len = + get_serialized_length(&traits, field_type, &context.def_interner) + .map_err(|err| (err, file_id))?; + + context.def_interner.update_expression(new_call_expression.arguments[1], |expr| { + *expr = HirExpression::Literal(HirLiteral::Integer( + FieldElement::from(new_storage_slot), + false, + )); + }); + + context.def_interner.update_expression(storage_layout_slot_expr, |expr| { + *expr = HirExpression::Literal(HirLiteral::Integer( + FieldElement::from(new_storage_slot), + false, + )); + }); + + storage_slot += type_serialized_len; } - .ok_or(( - AztecMacroError::CouldNotAssignStorageSlots { - secondary_message: Some(format!( - "Storage layout field ({}) not found or has an incorrect type", - field_name - )), - }, - file_id, - ))?; - - let new_storage_slot = if current_storage_slot == 0 { - u128::from(storage_slot) - } else { - current_storage_slot - }; - - let type_serialized_len = - get_serialized_length(&traits, field_type, &context.def_interner) - .map_err(|err| (err, file_id))?; - - context.def_interner.update_expression(new_call_expression.arguments[1], |expr| { - *expr = HirExpression::Literal(HirLiteral::Integer( - FieldElement::from(new_storage_slot), - false, - )); - }); - - context.def_interner.update_expression(storage_layout_slot_expr, |expr| { - *expr = HirExpression::Literal(HirLiteral::Integer( - FieldElement::from(new_storage_slot), - false, - )); - }); - - storage_slot += type_serialized_len; } } @@ -478,10 +470,10 @@ pub fn generate_storage_layout( let mut storable_fields_impl = vec![]; definition.fields.iter().enumerate().for_each(|(index, (field_ident, field_type))| { - storable_fields.push(format!("{}: Storable", field_ident, index)); + storable_fields.push(format!("{}: dep::aztec::prelude::Storable", field_ident, index)); generic_args.push(format!("N{}", index)); storable_fields_impl.push(format!( - "{}: Storable {{ slot: 0, typ: \"{}\" }}", + "{}: dep::aztec::prelude::Storable {{ slot: 0, typ: \"{}\" }}", field_ident, field_type.to_string().replace("plain::", "") )); diff --git a/noir/noir-repo/aztec_macros/src/utils/errors.rs b/noir/noir-repo/aztec_macros/src/utils/errors.rs index 9d7043d08eb0..e3a3db87a3dc 100644 --- a/noir/noir-repo/aztec_macros/src/utils/errors.rs +++ b/noir/noir-repo/aztec_macros/src/utils/errors.rs @@ -57,7 +57,7 @@ impl From for MacroError { }, AztecMacroError::CouldNotExportStorageLayout { secondary_message, span } => MacroError { primary_message: "Could not generate and export storage layout".to_string(), - secondary_message: secondary_message, + secondary_message, span, }, AztecMacroError::EventError { span, message } => MacroError { diff --git a/yarn-project/aztec.js/src/contract/contract_base.ts b/yarn-project/aztec.js/src/contract/contract_base.ts index 2767a42e0d1e..e4dca07a868c 100644 --- a/yarn-project/aztec.js/src/contract/contract_base.ts +++ b/yarn-project/aztec.js/src/contract/contract_base.ts @@ -1,4 +1,4 @@ -import { computePartialAddress } from '@aztec/circuits.js'; +import { Fr, computePartialAddress } from '@aztec/circuits.js'; import { ContractArtifact, FunctionArtifact, FunctionSelector } from '@aztec/foundation/abi'; import { ContractInstanceWithAddress } from '@aztec/types/contracts'; @@ -16,6 +16,48 @@ export type ContractMethod = ((...args: any[]) => ContractFunctionInteraction) & readonly selector: FunctionSelector; }; +/** + * Type representing a field layout in the storage of a contract. + */ +type FieldLayout = { + /** + * Slot in which the field is stored. + */ + slot: Fr; + /** + * Type being stored at the slot + */ + typ: string; +}; + +/** + * Type representing a note in use in the contract. + */ +type ContractNote = { + /** + * Note identifier + */ + id: Fr; + /** + * Type of the note + */ + typ: string; +}; + +/** + * Type representing the storage layout of a contract. + */ +export type ContractStorageLayout = { + [K in T]: FieldLayout; +}; + +/** + * Type representing the notes used in a contract. + */ +export type ContractNotes = { + [K in T]: ContractNote; +}; + /** * Abstract implementation of a contract extended by the Contract class and generated contract types. */ diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 8b7785c05848..1dce62390112 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -25,6 +25,8 @@ export { Contract, ContractBase, ContractMethod, + ContractStorageLayout, + ContractNotes, SentTx, BatchCall, DeployMethod, diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts index d1f01897e01a..f3e2a1334523 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract.test.ts @@ -38,16 +38,13 @@ describe('e2e_token_contract', () => { let tokenSim: TokenSimulator; const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(amount), secretHash]); const extendedNote = new ExtendedNote( note, accounts[accountIndex].address, asset.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TokenNote.id, txHash, ); await wallets[accountIndex].addNote(extendedNote); diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts index d2c1d7009019..6ec68958d30d 100644 --- a/yarn-project/foundation/src/abi/abi.ts +++ b/yarn-project/foundation/src/abi/abi.ts @@ -24,9 +24,11 @@ export type ABIValue = | FieldValue | StructValue; +export type TypedStructFieldValue = { name: string; value: T }; + export interface StructValue { kind: 'struct'; - fields: (ABIValue & { name: string })[]; + fields: TypedStructFieldValue[]; } export interface FieldValue extends BasicValue<'field', string> { diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 87e22fcb23e8..43989cb357dc 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -1,7 +1,11 @@ import { ABIParameter, + BasicType, + BasicValue, ContractArtifact, FunctionArtifact, + StructValue, + TypedStructFieldValue, getDefaultInitializer, isAztecAddressStruct, isEthAddressStruct, @@ -180,6 +184,93 @@ function generateAbiStatement(name: string, artifactImportPath: string) { return stmts.join('\n'); } +/** + * Generates a getter for the contract's storage layout. + * @param input - The contract artifact. + */ +function generateStorageLayoutGetter(input: ContractArtifact) { + const storage = input.outputs.globals.storage ? (input.outputs.globals.storage[0] as StructValue) : { fields: [] }; + const storageFields = storage.fields as TypedStructFieldValue[]; + const storageFieldsUnionType = storageFields.map(f => `'${f.name}'`).join(' | '); + return storageFields.length > 0 + ? ` + public static get storage(): ContractStorageLayout<${storageFieldsUnionType}> { + const storage = this.artifact.outputs.globals.storage + ? (this.artifact.outputs.globals.storage[0] as any) + : { fields: [] }; + const storageFields = storage.fields as any; + return storageFields.reduce( + ( + acc: any, + { + name, + value: { + fields: [slot, typ], + }, + }: { + name: string; + value: { + fields: [slot: { value: { value: bigint } }, typ: { value: { value: string } }]; + }; + }, + ) => { + return { + ...acc, + ...{ + [name]: { + slot: new Fr(slot.value.value), + typ: typ.value.value, + }, + }, + }; + }, + {}, + ) as ContractStorageLayout<${storageFieldsUnionType}>; + } + ` + : ''; +} + +/** + * Generates a getter for the contract notes + * @param input - The contract artifact. + */ +function generateNotesGetter(input: ContractArtifact) { + const notes = input.outputs.globals.notes ? (input.outputs.globals.notes as StructValue[]) : []; + const notesUnionType = notes.map(n => `'${(n.fields[1].value as BasicValue<'string', string>).value}'`).join(' | '); + return notes.length > 0 + ? ` + public static get notes(): ContractNotes<${notesUnionType}> { + const notes = this.artifact.outputs.globals.notes ? (this.artifact.outputs.globals.notes as any) : []; + return notes.reduce( + ( + acc: any, + { + value: { + fields: [id, name], + }, + }: { + value: { + fields: [id: { value: { value: bigint } }, typ: { value: { value: string } }]; + }; + }, + ) => { + return { + ...acc, + ...{ + [name.value.value]: { + id: new Fr(id.value.value), + }, + }, + }; + }, + {}, + ) as ContractNotes<${notesUnionType}>; + } + ` + : ''; +} + /** * Generates the typescript code to represent a contract. * @param input - The compiled Noir artifact. @@ -193,6 +284,8 @@ export function generateTypescriptContractInterface(input: ContractArtifact, art const at = artifactImportPath && generateAt(input.name); const artifactStatement = artifactImportPath && generateAbiStatement(input.name, artifactImportPath); const artifactGetter = artifactImportPath && generateArtifactGetter(input.name); + const storageLayoutGetter = artifactImportPath && generateStorageLayoutGetter(input); + const notesGetter = artifactImportPath && generateNotesGetter(input); return ` /* Autogenerated file, do not edit! */ @@ -208,6 +301,8 @@ import { ContractFunctionInteraction, ContractInstanceWithAddress, ContractMethod, + ContractStorageLayout, + ContractNotes, DeployMethod, EthAddress, EthAddressLike, @@ -235,6 +330,10 @@ export class ${input.name}Contract extends ContractBase { ${artifactGetter} + ${storageLayoutGetter} + + ${notesGetter} + /** Type-safe wrappers for the public methods exposed by the contract. */ public methods!: { ${methods.join('\n')}