From a501ebb2d007b9e3b16f0c987152a2b218b9a04b Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:20:35 +0000 Subject: [PATCH] feat: add str support for args + add name/symbol/decimal to token (#3862) Fixes #2889 by adding support for str as input arg and adding a compressed string struct. --- noir/aztec_macros/src/lib.rs | 31 +++- .../functions/ecdsa_secp256k1_verify.md | 1 - .../docs/reference/NoirJS/noir_js/index.md | 2 +- .../aztec/src/types/type_serialization.nr | 1 + .../type_serialization/u8_serialization.nr | 16 ++ .../aztec-nr/compressed-string/Nargo.toml | 9 ++ .../src/compressed_string.nr | 142 ++++++++++++++++++ .../aztec-nr/compressed-string/src/lib.nr | 4 + .../aztec-sandbox/src/examples/token.ts | 2 +- yarn-project/cli/src/encoding.ts | 2 + .../end-to-end/src/cli_docs_sandbox.test.ts | 3 +- .../end-to-end/src/e2e_2_pxes.test.ts | 2 +- .../end-to-end/src/e2e_block_building.test.ts | 2 +- .../end-to-end/src/e2e_cheat_codes.test.ts | 2 +- .../src/e2e_deploy_contract.test.ts | 14 +- .../src/e2e_escrow_contract.test.ts | 2 +- .../src/e2e_lending_contract.test.ts | 8 +- .../e2e_multiple_accounts_1_enc_key.test.ts | 2 +- .../src/e2e_sandbox_example.test.ts | 2 +- .../end-to-end/src/e2e_token_contract.test.ts | 79 ++++++++-- .../src/guides/dapp_testing.test.ts | 12 +- .../end-to-end/src/guides/up_quick_start.sh | 2 +- .../writing_an_account_contract.test.ts | 2 +- .../end-to-end/src/sample-dapp/deploy.mjs | 5 +- .../end-to-end/src/sample-dapp/index.test.mjs | 4 +- yarn-project/end-to-end/src/shared/browser.ts | 2 +- yarn-project/end-to-end/src/shared/cli.ts | 2 +- .../src/shared/cross_chain_test_harness.ts | 2 +- yarn-project/foundation/src/abi/decoder.ts | 13 +- .../foundation/src/abi/encoder.test.ts | 47 ++++++ yarn-project/foundation/src/abi/encoder.ts | 9 +- yarn-project/noir-contracts/.gitignore | 2 +- yarn-project/noir-contracts/Nargo.toml | 1 + .../contracts/reader_contract/Nargo.toml | 10 ++ .../contracts/reader_contract/src/main.nr | 37 +++++ .../contracts/token_contract/Nargo.toml | 1 + .../contracts/token_contract/src/main.nr | 69 ++++++++- 37 files changed, 503 insertions(+), 43 deletions(-) create mode 100644 yarn-project/aztec-nr/aztec/src/types/type_serialization/u8_serialization.nr create mode 100644 yarn-project/aztec-nr/compressed-string/Nargo.toml create mode 100644 yarn-project/aztec-nr/compressed-string/src/compressed_string.nr create mode 100644 yarn-project/aztec-nr/compressed-string/src/lib.nr create mode 100644 yarn-project/noir-contracts/contracts/reader_contract/Nargo.toml create mode 100644 yarn-project/noir-contracts/contracts/reader_contract/src/main.nr diff --git a/noir/aztec_macros/src/lib.rs b/noir/aztec_macros/src/lib.rs index 3bba3c7adfc..3aa92c03388 100644 --- a/noir/aztec_macros/src/lib.rs +++ b/noir/aztec_macros/src/lib.rs @@ -620,7 +620,21 @@ fn create_context(ty: &str, params: &[Param]) -> Vec { UnresolvedTypeData::Integer(..) | UnresolvedTypeData::Bool => { add_cast_to_hasher(identifier) } - _ => unreachable!("[Aztec Noir] Provided parameter type is not supported"), + UnresolvedTypeData::String(..) => { + let (var_bytes, id) = str_to_bytes(identifier); + injected_expressions.push(var_bytes); + add_array_to_hasher( + &id, + &UnresolvedType { + typ: UnresolvedTypeData::Integer(Signedness::Unsigned, 32), + span: None, + }, + ) + } + _ => panic!( + "[Aztec Noir] Provided parameter type: {:?} is not supported", + unresolved_type + ), }; injected_expressions.push(expression); } @@ -909,6 +923,21 @@ fn add_struct_to_hasher(identifier: &Ident) -> Statement { ))) } +fn str_to_bytes(identifier: &Ident) -> (Statement, Ident) { + // let identifier_as_bytes = identifier.as_bytes(); + let var = variable_ident(identifier.clone()); + let contents = if let ExpressionKind::Variable(p) = &var.kind { + p.segments.first().cloned().unwrap_or_else(|| panic!("No segments")).0.contents + } else { + panic!("Unexpected identifier type") + }; + let bytes_name = format!("{}_bytes", contents); + let var_bytes = assignment(&bytes_name, method_call(var, "as_bytes", vec![])); + let id = Ident::new(bytes_name, Span::default()); + + (var_bytes, id) +} + fn create_loop_over(var: Expression, loop_body: Vec) -> Statement { // If this is an array of primitive types (integers / fields) we can add them each to the hasher // casted to a field diff --git a/noir/docs/docs/reference/NoirJS/noir_js/functions/ecdsa_secp256k1_verify.md b/noir/docs/docs/reference/NoirJS/noir_js/functions/ecdsa_secp256k1_verify.md index 0ba5783f0d5..5e3cd53e9d3 100644 --- a/noir/docs/docs/reference/NoirJS/noir_js/functions/ecdsa_secp256k1_verify.md +++ b/noir/docs/docs/reference/NoirJS/noir_js/functions/ecdsa_secp256k1_verify.md @@ -8,7 +8,6 @@ ecdsa_secp256k1_verify( signature): boolean ``` -Calculates the Blake2s256 hash of the input bytes and represents these as a single field element. Verifies a ECDSA signature over the secp256k1 curve. ## Parameters diff --git a/noir/docs/docs/reference/NoirJS/noir_js/index.md b/noir/docs/docs/reference/NoirJS/noir_js/index.md index 8b9e35bc9a1..d600e21b299 100644 --- a/noir/docs/docs/reference/NoirJS/noir_js/index.md +++ b/noir/docs/docs/reference/NoirJS/noir_js/index.md @@ -26,7 +26,7 @@ | :------ | :------ | | [and](functions/and.md) | Performs a bitwise AND operation between `lhs` and `rhs` | | [blake2s256](functions/blake2s256.md) | Calculates the Blake2s256 hash of the input bytes | -| [ecdsa\_secp256k1\_verify](functions/ecdsa_secp256k1_verify.md) | Calculates the Blake2s256 hash of the input bytes and represents these as a single field element. | +| [ecdsa\_secp256k1\_verify](functions/ecdsa_secp256k1_verify.md) | Verifies a ECDSA signature over the secp256k1 curve. | | [ecdsa\_secp256r1\_verify](functions/ecdsa_secp256r1_verify.md) | Verifies a ECDSA signature over the secp256r1 curve. | | [keccak256](functions/keccak256.md) | Calculates the Keccak256 hash of the input bytes | | [sha256](functions/sha256.md) | Calculates the SHA256 hash of the input bytes | diff --git a/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr b/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr index 0d15ed61441..016548a544f 100644 --- a/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr +++ b/yarn-project/aztec-nr/aztec/src/types/type_serialization.nr @@ -1,5 +1,6 @@ mod bool_serialization; mod field_serialization; +mod u8_serialization; mod u32_serialization; mod address_serialization; diff --git a/yarn-project/aztec-nr/aztec/src/types/type_serialization/u8_serialization.nr b/yarn-project/aztec-nr/aztec/src/types/type_serialization/u8_serialization.nr new file mode 100644 index 00000000000..8011d02675c --- /dev/null +++ b/yarn-project/aztec-nr/aztec/src/types/type_serialization/u8_serialization.nr @@ -0,0 +1,16 @@ +use crate::types::type_serialization::TypeSerializationInterface; + +global U8_SERIALIZED_LEN: Field = 1; + +fn deserializeU8(fields: [Field; U8_SERIALIZED_LEN]) -> u8 { + fields[0] as u8 +} + +fn serializeU8(value: u8) -> [Field; U8_SERIALIZED_LEN] { + [value as Field] +} + +global U8SerializationMethods = TypeSerializationInterface { + deserialize: deserializeU8, + serialize: serializeU8, +}; diff --git a/yarn-project/aztec-nr/compressed-string/Nargo.toml b/yarn-project/aztec-nr/compressed-string/Nargo.toml new file mode 100644 index 00000000000..431e8d34395 --- /dev/null +++ b/yarn-project/aztec-nr/compressed-string/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "compressed_string" +authors = [""] +compiler_version = ">=0.18.0" +type = "lib" + +[dependencies] +aztec = {path = "../aztec"} +protocol_types = {path = "../../noir-protocol-circuits/src/crates/types"} diff --git a/yarn-project/aztec-nr/compressed-string/src/compressed_string.nr b/yarn-project/aztec-nr/compressed-string/src/compressed_string.nr new file mode 100644 index 00000000000..b81d86648eb --- /dev/null +++ b/yarn-project/aztec-nr/compressed-string/src/compressed_string.nr @@ -0,0 +1,142 @@ +use dep::aztec::types::type_serialization::TypeSerializationInterface; +use dep::protocol_types::utils::field::field_from_bytes; +use dep::std; + +// A Fixedsize Compressed String. +// Essentially a special version of Compressed String for practical use. +struct FieldCompressedString{ + value: Field +} + +impl FieldCompressedString{ + pub fn is_eq(self, other: FieldCompressedString) -> bool { + self.value == other.value + } + + pub fn from_field(input_field: Field) -> Self { + Self {value: input_field} + } + + pub fn from_string(input_string: str<31>) -> Self { + Self {value: field_from_bytes(input_string.as_bytes(), true)} + } + + pub fn to_bytes(self) -> [u8; 31] { + let mut result = [0; 31]; + let bytes = self.value.to_be_bytes(31); + for i in 0..31 { + result[i] = bytes[i]; + } + result + } + + pub fn serialize(self) -> [Field; 1] { + [self.value] + } + + pub fn deserialize(input: [Field; 1]) -> Self { + Self { value: input[0] } + } +} + +fn deserialize(fields: [Field; 1]) -> FieldCompressedString { + FieldCompressedString { value: fields[0] } +} + +fn serialize(value: FieldCompressedString) -> [Field; 1] { + value.serialize() +} +global FieldCompressedStringSerializationMethods = TypeSerializationInterface { + deserialize, + serialize, +}; + +// The general Compressed String. +// Compresses M bytes into N fields. +// Can be used for longer strings that don't fit in a single field. +// Each field can store 31 characters, so N should be M/31 rounded up. +struct CompressedString { + value: [Field; N] +} + +impl CompressedString { + pub fn from_string(input_string: str) -> Self { + let mut fields = [0; N]; + let byts = input_string.as_bytes(); + + let mut r_index = 0 as u32; + + for i in 0..N { + let mut temp = [0 as u8; 31]; + for j in 0..31 { + if r_index < M { + temp[j] = byts[r_index]; + r_index += 1; + } + } + + fields[i] = field_from_bytes(temp, true); + } + + Self { value: fields } + } + + pub fn to_bytes(self) -> [u8; M] { + let mut result = [0; M]; + let mut w_index = 0 as u32; + for i in 0..N { + let bytes = self.value[i].to_be_bytes(31); + for j in 0..31 { + if w_index < M { + result[w_index] = bytes[j]; + w_index += 1; + } + } + } + result + } + + pub fn serialize(self) -> [Field; N] { + self.value + } + + pub fn deserialize(input: [Field; N]) -> Self { + Self { value: input } + } +} + +#[test] +fn test_short_string() { + let i = "Hello world"; + let b = i.as_bytes(); + let name: CompressedString<1,11> = CompressedString::from_string(i); + let p = b == name.to_bytes(); + assert(p, "invalid recover"); +} + +#[test] +fn test_long_string() { + let i = "Hello world. I'm setting up a very long text of blibbablubb such that we can see if works as planned for longer names."; + let b = i.as_bytes(); + let name: CompressedString<4,118> = CompressedString::from_string(i); + let p = b == name.to_bytes(); + assert(p, "invalid recover"); +} + +#[test] +fn test_long_string_work_with_too_many_fields() { + let i = "Hello world. I'm setting up a very long text of blibbablubb such that we can see if works as planned for longer names."; + let b = i.as_bytes(); + let name: CompressedString<5,118> = CompressedString::from_string(i); + let p = b == name.to_bytes(); + assert(p, "invalid recover"); +} + +#[test(should_fail)] +fn test_long_string_fail_with_too_few_fields() { + let i = "Hello world. I'm setting up a very long text of blibbablubb such that we can see if works as planned for longer names."; + let b = i.as_bytes(); + let name: CompressedString<3,118> = CompressedString::from_string(i); + let p = b == name.to_bytes(); + assert(p, "invalid recover"); +} diff --git a/yarn-project/aztec-nr/compressed-string/src/lib.nr b/yarn-project/aztec-nr/compressed-string/src/lib.nr new file mode 100644 index 00000000000..aef2a573fdf --- /dev/null +++ b/yarn-project/aztec-nr/compressed-string/src/lib.nr @@ -0,0 +1,4 @@ +mod compressed_string; + +use crate::compressed_string::{CompressedString}; +use crate::compressed_string::{FieldCompressedString, FieldCompressedStringSerializationMethods}; diff --git a/yarn-project/aztec-sandbox/src/examples/token.ts b/yarn-project/aztec-sandbox/src/examples/token.ts index 816bae98370..96ab05f435c 100644 --- a/yarn-project/aztec-sandbox/src/examples/token.ts +++ b/yarn-project/aztec-sandbox/src/examples/token.ts @@ -33,7 +33,7 @@ async function main() { logger(`Created Alice and Bob accounts: ${alice.address.toString()}, ${bob.address.toString()}`); logger('Deploying Token...'); - const token = await TokenContract.deploy(aliceWallet, alice).send().deployed(); + const token = await TokenContract.deploy(aliceWallet, alice, 'TokenName', 'TokenSymbol', 18).send().deployed(); logger('Token deployed'); // Create the contract abstraction and link it to Alice's and Bob's wallet for future signing diff --git a/yarn-project/cli/src/encoding.ts b/yarn-project/cli/src/encoding.ts index d903c2fd234..21ecba9fc8a 100644 --- a/yarn-project/cli/src/encoding.ts +++ b/yarn-project/cli/src/encoding.ts @@ -50,6 +50,8 @@ function encodeArg(arg: string, abiType: ABIType, name: string): any { } else { throw Error(`Invalid boolean value passed for ${name}: ${arg}.`); } + } else if (kind === 'string') { + return arg; } else if (kind === 'array') { let arr; const res = []; diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index a122f12ca61..46411f3e3af 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -112,6 +112,7 @@ LendingContractArtifact ParentContractArtifact PendingCommitmentsContractArtifact PriceFeedContractArtifact +ReaderContractArtifact SchnorrAccountContractArtifact SchnorrHardcodedAccountContractArtifact SchnorrSingleKeyAccountContractArtifact @@ -255,7 +256,7 @@ Accounts found: // Test deploy docs = ` // docs:start:deploy -% aztec-cli deploy TokenContractArtifact --args $ADDRESS +% aztec-cli deploy TokenContractArtifact --args $ADDRESS TokenName TKN 18 Contract deployed at 0x1ae8eea0dc265fb7f160dae62cc8912686d8a9ed78e821fbdd8bcedc54c06d0f // docs:end:deploy diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index 60d3bcbfe4a..a04170dd66e 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -91,7 +91,7 @@ describe('e2e_2_pxes', () => { const deployTokenContract = async (initialAdminBalance: bigint, admin: AztecAddress, pxe: PXE) => { logger(`Deploying Token contract...`); - const contract = await TokenContract.deploy(walletA, admin).send().deployed(); + const contract = await TokenContract.deploy(walletA, admin, 'TokenName', 'TokenSymbol', 18).send().deployed(); if (initialAdminBalance > 0n) { await mintTokens(contract, admin, initialAdminBalance, pxe); diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index 699e634099e..8cf476f323c 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -80,7 +80,7 @@ describe('e2e_block_building', () => { // Deploy a contract in the first transaction // In the same block, call a public method on the contract - const deployer = TokenContract.deploy(owner, owner.getCompleteAddress()); + const deployer = TokenContract.deploy(owner, owner.getCompleteAddress(), 'TokenName', 'TokenSymbol', 18); await deployer.create(); // We can't use `TokenContract.at` to call a function because it checks the contract is deployed diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index f83fd2cb22b..d28ec8e086a 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -39,7 +39,7 @@ describe('e2e_cheat_codes', () => { rollupAddress = deployL1ContractsValues.l1ContractAddresses.rollupAddress; admin = accounts[0]; - token = await TokenContract.deploy(wallet, admin).send().deployed(); + token = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send().deployed(); }, 100_000); afterAll(() => teardown()); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts index 426ab8b4053..b3b66639898 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts @@ -145,8 +145,18 @@ describe('e2e_deploy_contract', () => { try { // This test requires at least another good transaction to go through in the same block as the bad one. // I deployed the same contract again but it could really be any valid transaction here. - const goodDeploy = new ContractDeployer(TokenContractArtifact, wallet).deploy(AztecAddress.random()); - const badDeploy = new ContractDeployer(TokenContractArtifact, wallet).deploy(AztecAddress.ZERO); + const goodDeploy = new ContractDeployer(TokenContractArtifact, wallet).deploy( + AztecAddress.random(), + 'TokenName', + 'TKN', + 18, + ); + const badDeploy = new ContractDeployer(TokenContractArtifact, wallet).deploy( + AztecAddress.ZERO, + 'TokenName', + 'TKN', + 18, + ); await Promise.all([ goodDeploy.simulate({ skipPublicSimulation: true }), diff --git a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts index 660cfd9b33a..28d529572f5 100644 --- a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts @@ -64,7 +64,7 @@ describe('e2e_escrow_contract', () => { logger(`Escrow contract deployed at ${escrowContract.address}`); // Deploy Token contract and mint funds for the escrow contract - token = await TokenContract.deploy(wallet, owner).send().deployed(); + token = await TokenContract.deploy(wallet, owner, 'TokenName', 'TokenSymbol', 18).send().deployed(); const mintAmount = 100n; const secret = Fr.random(); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index ad97dee72d2..77c58873e46 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -58,14 +58,18 @@ describe('e2e_lending_contract', () => { { logger(`Deploying collateral asset feed contract...`); - const receipt = await waitForSuccess(TokenContract.deploy(wallet, accounts[0]).send()); + const receipt = await waitForSuccess( + TokenContract.deploy(wallet, accounts[0], 'TokenName', 'TokenSymbol', 18).send(), + ); logger(`Collateral asset deployed to ${receipt.contractAddress}`); collateralAsset = await TokenContract.at(receipt.contractAddress!, wallet); } { logger(`Deploying stable coin contract...`); - const receipt = await waitForSuccess(TokenContract.deploy(wallet, accounts[0]).send()); + const receipt = await waitForSuccess( + TokenContract.deploy(wallet, accounts[0], 'TokenName', 'TokenSymbol', 18).send(), + ); logger(`Stable coin asset deployed to ${receipt.contractAddress}`); stableCoin = await TokenContract.at(receipt.contractAddress!, wallet); } diff --git a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts index 4614d16de93..08acbdf58f7 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts @@ -55,7 +55,7 @@ describe('e2e_multiple_accounts_1_enc_key', () => { } logger(`Deploying Token...`); - const token = await TokenContract.deploy(wallets[0], accounts[0]).send().deployed(); + const token = await TokenContract.deploy(wallets[0], accounts[0], 'TokenName', 'TokenSymbol', 18).send().deployed(); tokenAddress = token.address; logger(`Token deployed at ${tokenAddress}`); diff --git a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts b/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts index ed309ad1361..e92b85608aa 100644 --- a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts +++ b/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts @@ -61,7 +61,7 @@ describe('e2e_sandbox_example', () => { logger(`Deploying token contract...`); // Deploy the contract and set Alice as the admin while doing so - const contract = await TokenContract.deploy(aliceWallet, alice).send().deployed(); + const contract = await TokenContract.deploy(aliceWallet, alice, 'TokenName', 'TokenSymbol', 18).send().deployed(); logger(`Contract successfully deployed at address ${contract.address.toShortString()}`); // Create the contract abstraction and link it to Alice's wallet for future signing 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 012bb273bed..33ecf0da53a 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 @@ -11,6 +11,8 @@ import { computeAuthWitMessageHash, computeMessageSecretHash, } from '@aztec/aztec.js'; +import { decodeFunctionSignature } from '@aztec/foundation/abi'; +import { ReaderContract } from '@aztec/noir-contracts/Reader'; import { TokenContract } from '@aztec/noir-contracts/Token'; import { jest } from '@jest/globals'; @@ -23,6 +25,9 @@ const TIMEOUT = 90_000; describe('e2e_token_contract', () => { jest.setTimeout(TIMEOUT); + const TOKEN_NAME = 'Aztec Token'; + const TOKEN_SYMBOL = 'AZT'; + const TOKEN_DECIMALS = 18n; let teardown: () => Promise; let wallets: AccountWallet[]; let accounts: CompleteAddress[]; @@ -39,10 +44,27 @@ describe('e2e_token_contract', () => { await wallets[accountIndex].addNote(extendedNote); }; + const toString = (val: bigint[]) => { + let str = ''; + for (let i = 0; i < val.length; i++) { + if (val[i] != 0n) { + str += String.fromCharCode(Number(val[i])); + } + } + return str; + }; + beforeAll(async () => { ({ teardown, logger, wallets, accounts } = await setup(3)); - asset = await TokenContract.deploy(wallets[0], accounts[0]).send().deployed(); + TokenContract.artifact.functions.forEach(fn => { + const sig = decodeFunctionSignature(fn.name, fn.parameters); + logger(`Function ${sig} and the selector: ${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)}`); + }); + + asset = await TokenContract.deploy(wallets[0], accounts[0], TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) + .send() + .deployed(); logger(`Token deployed to ${asset.address}`); tokenSim = new TokenSimulator( asset, @@ -51,15 +73,6 @@ describe('e2e_token_contract', () => { ); expect(await asset.methods.admin().view()).toBe(accounts[0].address.toBigInt()); - - asset.artifact.functions.forEach(fn => { - logger( - `Function ${fn.name} has ${fn.bytecode.length} bytes and the selector: ${FunctionSelector.fromNameAndParameters( - fn.name, - fn.parameters, - )}`, - ); - }); }, 100_000); afterAll(() => teardown()); @@ -68,6 +81,52 @@ describe('e2e_token_contract', () => { await tokenSim.check(); }, TIMEOUT); + describe('Reading constants', () => { + let reader: ReaderContract; + beforeAll(async () => { + reader = await ReaderContract.deploy(wallets[0]).send().deployed(); + }); + + it('name', async () => { + const t = toString(await asset.methods.un_get_name().view()); + expect(t).toBe(TOKEN_NAME); + + const tx = reader.methods.check_name(asset.address, TOKEN_NAME).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + await expect(reader.methods.check_name(asset.address, 'WRONG_NAME').simulate()).rejects.toThrowError( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'name.is_eq(_what)'", + ); + }); + + it('symbol', async () => { + const t = toString(await asset.methods.un_get_symbol().view()); + expect(t).toBe(TOKEN_SYMBOL); + + const tx = reader.methods.check_symbol(asset.address, TOKEN_SYMBOL).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + await expect(reader.methods.check_symbol(asset.address, 'WRONG_SYMBOL').simulate()).rejects.toThrowError( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'symbol.is_eq(_what)'", + ); + }); + + it('decimals', async () => { + const t = await asset.methods.un_get_decimals().view(); + expect(t).toBe(TOKEN_DECIMALS); + + const tx = reader.methods.check_decimals(asset.address, TOKEN_DECIMALS).send(); + const receipt = await tx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); + + await expect(reader.methods.check_decimals(asset.address, 99).simulate()).rejects.toThrowError( + "Failed to solve brillig function, reason: explicit trap hit in brillig 'ret[0] as u8 == what'", + ); + }); + }); + describe('Access controlled functions', () => { it('Set admin', async () => { const tx = asset.methods.set_admin(accounts[1].address).send(); diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 74c7d0f9793..203fda7ed9a 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -33,7 +33,9 @@ describe('guides/dapp/testing', () => { pxe = createPXEClient(PXE_URL); owner = await createAccount(pxe); recipient = await createAccount(pxe); - token = await TokenContract.deploy(owner, owner.getCompleteAddress()).send().deployed(); + token = await TokenContract.deploy(owner, owner.getCompleteAddress(), 'TokenName', 'TokenSymbol', 18) + .send() + .deployed(); }, 60_000); it('increases recipient funds on mint', async () => { @@ -66,7 +68,9 @@ describe('guides/dapp/testing', () => { // docs:start:use-existing-wallets pxe = createPXEClient(PXE_URL); [owner, recipient] = await getDeployedTestAccountsWallets(pxe); - token = await TokenContract.deploy(owner, owner.getCompleteAddress()).send().deployed(); + token = await TokenContract.deploy(owner, owner.getCompleteAddress(), 'TokenName', 'TokenSymbol', 18) + .send() + .deployed(); // docs:end:use-existing-wallets }, 30_000); @@ -124,7 +128,9 @@ describe('guides/dapp/testing', () => { owner = await createAccount(pxe); recipient = await createAccount(pxe); testContract = await TestContract.deploy(owner).send().deployed(); - token = await TokenContract.deploy(owner, owner.getCompleteAddress()).send().deployed(); + token = await TokenContract.deploy(owner, owner.getCompleteAddress(), 'TokenName', 'TokenSymbol', 18) + .send() + .deployed(); const ownerAddress = owner.getAddress(); const mintAmount = 100n; diff --git a/yarn-project/end-to-end/src/guides/up_quick_start.sh b/yarn-project/end-to-end/src/guides/up_quick_start.sh index 418952234bf..badbad7a9e8 100755 --- a/yarn-project/end-to-end/src/guides/up_quick_start.sh +++ b/yarn-project/end-to-end/src/guides/up_quick_start.sh @@ -12,7 +12,7 @@ ALICE_PRIVATE_KEY="0x2153536ff6628eee01cf4024889ff977a18d9fa61d0e414422f7681cf08 # docs:end:declare-accounts # docs:start:deploy -CONTRACT=$(aztec-cli deploy TokenContractArtifact --salt 0 --args $ALICE --json | jq -r '.address') +CONTRACT=$(aztec-cli deploy TokenContractArtifact --salt 0 --args $ALICE "TokenName" "TKN" 18 --json | jq -r '.address') echo "Deployed contract at $CONTRACT" aztec-cli check-deploy --contract-address $CONTRACT # docs:end:deploy diff --git a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts index 974f0f5e7a1..497d7442f64 100644 --- a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts +++ b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts @@ -64,7 +64,7 @@ describe('guides/writing_an_account_contract', () => { logger(`Deployed account contract at ${address}`); // docs:start:account-contract-works - const token = await TokenContract.deploy(wallet, { address }).send().deployed(); + const token = await TokenContract.deploy(wallet, { address }, 'TokenName', 'TokenSymbol', 18).send().deployed(); logger(`Deployed token contract at ${token.address}`); const secret = Fr.random(); diff --git a/yarn-project/end-to-end/src/sample-dapp/deploy.mjs b/yarn-project/end-to-end/src/sample-dapp/deploy.mjs index f3043b64d2b..35654abcac2 100644 --- a/yarn-project/end-to-end/src/sample-dapp/deploy.mjs +++ b/yarn-project/end-to-end/src/sample-dapp/deploy.mjs @@ -13,7 +13,10 @@ async function main() { const [ownerWallet] = await getInitialTestAccountsWallets(pxe); const ownerAddress = ownerWallet.getCompleteAddress(); - const token = await Contract.deploy(ownerWallet, TokenContractArtifact, [ownerAddress]).send().deployed(); + const token = await Contract.deploy(ownerWallet, TokenContractArtifact, [ownerAddress, 'TokenName', 'TKN', 18]) + .send() + .deployed(); + console.log(`Token deployed at ${token.address.toString()}`); const addresses = { token: token.address.toString() }; diff --git a/yarn-project/end-to-end/src/sample-dapp/index.test.mjs b/yarn-project/end-to-end/src/sample-dapp/index.test.mjs index 7724756ef16..ce8294ddbd3 100644 --- a/yarn-project/end-to-end/src/sample-dapp/index.test.mjs +++ b/yarn-project/end-to-end/src/sample-dapp/index.test.mjs @@ -21,7 +21,9 @@ describe('token', () => { owner = await createAccount(pxe); recipient = await createAccount(pxe); - token = await Contract.deploy(owner, TokenContractArtifact, [owner.getCompleteAddress()]).send().deployed(); + token = await Contract.deploy(owner, TokenContractArtifact, [owner.getCompleteAddress(), 'TokenName', 'TKN', 18]) + .send() + .deployed(); const initialBalance = 20n; const secret = Fr.random(); diff --git a/yarn-project/end-to-end/src/shared/browser.ts b/yarn-project/end-to-end/src/shared/browser.ts index 164474f899f..6dc060470fa 100644 --- a/yarn-project/end-to-end/src/shared/browser.ts +++ b/yarn-project/end-to-end/src/shared/browser.ts @@ -242,7 +242,7 @@ export const browserTestSuite = ( pxe, TokenContractArtifact, (a: AztecJs.AztecAddress) => Contract.at(a, TokenContractArtifact, owner), - [owner.getCompleteAddress()], + [owner.getCompleteAddress(), 'TokenName', 'TKN', 18], ).send(); const { contract: token, txHash } = await tx.wait(); diff --git a/yarn-project/end-to-end/src/shared/cli.ts b/yarn-project/end-to-end/src/shared/cli.ts index 75a95afbaab..bf500b841d8 100644 --- a/yarn-project/end-to-end/src/shared/cli.ts +++ b/yarn-project/end-to-end/src/shared/cli.ts @@ -130,7 +130,7 @@ export const cliTestSuite = ( const ownerAddress = AztecAddress.fromString(foundAddress!); debug('Deploy Token Contract using created account.'); - await run(`deploy TokenContractArtifact --salt 0 --args ${ownerAddress}`); + await run(`deploy TokenContractArtifact --salt 0 --args ${ownerAddress} 'TokenName' 'TKN' 18`); const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; expect(loggedAddress).toBeDefined(); contractAddress = AztecAddress.fromString(loggedAddress!); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index 5931be88bd5..db556b0ffe2 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -86,7 +86,7 @@ export async function deployAndInitializeTokenAndBridgeContracts( }); // deploy l2 token - const deployTx = TokenContract.deploy(wallet, owner).send(); + const deployTx = TokenContract.deploy(wallet, owner, 'TokenName', 'TokenSymbol', 18).send(); // now wait for the deploy txs to be mined. This way we send all tx in the same rollup. const deployReceipt = await deployTx.wait(); diff --git a/yarn-project/foundation/src/abi/decoder.ts b/yarn-project/foundation/src/abi/decoder.ts index cd37ba361e5..ac509d59ba1 100644 --- a/yarn-project/foundation/src/abi/decoder.ts +++ b/yarn-project/foundation/src/abi/decoder.ts @@ -43,8 +43,15 @@ class ReturnValuesDecoder { } return struct; } + case 'string': { + const array = []; + for (let i = 0; i < abiType.length; i += 1) { + array.push(this.getNextField().toBigInt()); + } + return array; + } default: - throw new Error(`Unsupported type: ${abiType.kind}`); + throw new Error(`Unsupported type: ${abiType}`); } } @@ -114,10 +121,12 @@ export class FunctionSignatureDecoder { return 'bool'; case 'array': return `[${this.getParameterType(param.type)};${param.length}]`; + case 'string': + return `str<${param.length}>`; case 'struct': return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`; default: - throw new Error(`Unsupported type: ${param.kind}`); + throw new Error(`Unsupported type: ${param}`); } } diff --git a/yarn-project/foundation/src/abi/encoder.test.ts b/yarn-project/foundation/src/abi/encoder.test.ts index faeac04f45d..4dd34529039 100644 --- a/yarn-project/foundation/src/abi/encoder.test.ts +++ b/yarn-project/foundation/src/abi/encoder.test.ts @@ -25,6 +25,53 @@ describe('abi/encoder', () => { expect(encodeArguments(abi, [field])).toEqual([field]); }); + it('serializes arrays of fields', () => { + const abi: FunctionAbi = { + name: 'constructor', + functionType: FunctionType.SECRET, + isInternal: false, + parameters: [ + { + name: 'owner', + type: { + kind: 'array', + length: 2, + type: { kind: 'field' }, + }, + visibility: ABIParameterVisibility.SECRET, + }, + ], + returnTypes: [], + }; + + const arr = [Fr.random(), Fr.random()]; + expect(encodeArguments(abi, [arr])).toEqual(arr); + }); + + it('serializes string', () => { + const abi: FunctionAbi = { + name: 'constructor', + functionType: FunctionType.SECRET, + isInternal: false, + parameters: [ + { + name: 'owner', + type: { + kind: 'string', + length: 4, + }, + visibility: ABIParameterVisibility.SECRET, + }, + ], + returnTypes: [], + }; + + const str = 'abc'; + // As bigints padded with 0 for length 4. ("a" = 97, "b" = 98, "c" = 99, 0) + const expected = [new Fr(97), new Fr(98), new Fr(99), new Fr(0)]; + expect(encodeArguments(abi, [str])).toEqual(expected); + }); + it.each(['AztecAddress', 'EthAddress'])('accepts address instance for %s structs', (structType: string) => { const abi: FunctionAbi = { name: 'constructor', diff --git a/yarn-project/foundation/src/abi/encoder.ts b/yarn-project/foundation/src/abi/encoder.ts index 6aece06f6c0..cf25db56f54 100644 --- a/yarn-project/foundation/src/abi/encoder.ts +++ b/yarn-project/foundation/src/abi/encoder.ts @@ -68,6 +68,13 @@ class ArgumentEncoder { this.encodeArgument(abiType.type, arg[i], `${name}[${i}]`); } break; + case 'string': + for (let i = 0; i < abiType.length; i += 1) { + // If the string is shorter than the defined length, pad it with 0s. + const toInsert = i < arg.length ? BigInt((arg as string).charCodeAt(i)) : 0n; + this.flattened.push(new Fr(toInsert)); + } + break; case 'struct': { // If the abi expects a struct like { address: Field } and the supplied arg does not have // an address field in it, we try to encode it as if it were a field directly. @@ -98,7 +105,7 @@ class ArgumentEncoder { this.flattened.push(new Fr(arg)); break; default: - throw new Error(`Unsupported type: ${abiType.kind}`); + throw new Error(`Unsupported type: ${abiType}`); } } diff --git a/yarn-project/noir-contracts/.gitignore b/yarn-project/noir-contracts/.gitignore index bfb0e3cb9b9..104562d8a94 100644 --- a/yarn-project/noir-contracts/.gitignore +++ b/yarn-project/noir-contracts/.gitignore @@ -1,2 +1,2 @@ target/ -src/ +/src diff --git a/yarn-project/noir-contracts/Nargo.toml b/yarn-project/noir-contracts/Nargo.toml index cd96b50eeb2..1b980b3dad3 100644 --- a/yarn-project/noir-contracts/Nargo.toml +++ b/yarn-project/noir-contracts/Nargo.toml @@ -25,4 +25,5 @@ members = [ "contracts/token_blacklist_contract", "contracts/token_bridge_contract", "contracts/uniswap_contract", + "contracts/reader_contract", ] diff --git a/yarn-project/noir-contracts/contracts/reader_contract/Nargo.toml b/yarn-project/noir-contracts/contracts/reader_contract/Nargo.toml new file mode 100644 index 00000000000..5dcaa989be8 --- /dev/null +++ b/yarn-project/noir-contracts/contracts/reader_contract/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "reader_contract" +authors = [""] +compiler_version = ">=0.18.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +protocol_types = { path = "../../../noir-protocol-circuits/src/crates/types" } +compressed_string = {path = "../../../aztec-nr/compressed-string"} \ No newline at end of file diff --git a/yarn-project/noir-contracts/contracts/reader_contract/src/main.nr b/yarn-project/noir-contracts/contracts/reader_contract/src/main.nr new file mode 100644 index 00000000000..cd98eab82b1 --- /dev/null +++ b/yarn-project/noir-contracts/contracts/reader_contract/src/main.nr @@ -0,0 +1,37 @@ +contract Reader { + use dep::protocol_types::{ + address::AztecAddress, + abis::function_selector::FunctionSelector, + }; + + use dep::compressed_string::{FieldCompressedString, FieldCompressedStringSerializationMethods}; + + #[aztec(private)] + + fn constructor() {} + + #[aztec(public)] + fn check_name(who: AztecAddress, what: str<31>) { + let selector = FunctionSelector::from_signature("public_get_name()"); + let ret = context.call_public_function_no_args(who, selector); + let name = FieldCompressedString::from_field(ret[0]); + let _what = FieldCompressedString::from_string(what); + assert(name.is_eq(_what)); + } + + #[aztec(public)] + fn check_symbol(who: AztecAddress, what: str<31>) { + let selector = FunctionSelector::from_signature("public_get_symbol()"); + let ret = context.call_public_function_no_args(who, selector); + let symbol = FieldCompressedString::from_field(ret[0]); + let _what = FieldCompressedString::from_string(what); + assert(symbol.is_eq(_what)); + } + + #[aztec(public)] + fn check_decimals(who: AztecAddress, what: u8) { + let selector = FunctionSelector::from_signature("public_get_decimals()"); + let ret = context.call_public_function_no_args(who, selector); + assert(ret[0] as u8 == what); + } +} diff --git a/yarn-project/noir-contracts/contracts/token_contract/Nargo.toml b/yarn-project/noir-contracts/contracts/token_contract/Nargo.toml index 5b53b775456..45bee129e85 100644 --- a/yarn-project/noir-contracts/contracts/token_contract/Nargo.toml +++ b/yarn-project/noir-contracts/contracts/token_contract/Nargo.toml @@ -7,5 +7,6 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } safe_math = { path = "../../../aztec-nr/safe-math" } +compressed_string = {path = "../../../aztec-nr/compressed-string"} authwit = { path = "../../../aztec-nr/authwit" } protocol_types = { path = "../../../noir-protocol-circuits/src/crates/types" } \ No newline at end of file diff --git a/yarn-project/noir-contracts/contracts/token_contract/src/main.nr b/yarn-project/noir-contracts/contracts/token_contract/src/main.nr index 8710aca8dd7..be8d77bdde6 100644 --- a/yarn-project/noir-contracts/contracts/token_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/token_contract/src/main.nr @@ -14,6 +14,7 @@ contract Token { use dep::std::option::Option; use dep::safe_math::SafeU120; + use dep::compressed_string::{FieldCompressedString, FieldCompressedStringSerializationMethods}; use dep::aztec::{ note::{ @@ -28,6 +29,7 @@ contract Token { field_serialization::{FieldSerializationMethods, FIELD_SERIALIZED_LEN}, bool_serialization::{BoolSerializationMethods, BOOL_SERIALIZED_LEN}, address_serialization::{AddressSerializationMethods, AZTEC_ADDRESS_SERIALIZED_LEN}, + u8_serialization::{U8SerializationMethods, U8_SERIALIZED_LEN}, }, }; use dep::protocol_types::{ @@ -68,6 +70,9 @@ contract Token { pending_shields: Set, // docs:end:storage_pending_shields public_balances: Map>, + symbol: PublicState, + name: PublicState, + decimals: PublicState, } // docs:end:storage_struct @@ -117,6 +122,21 @@ contract Token { ) }, ), + symbol: PublicState::new( + context, + 7, + FieldCompressedStringSerializationMethods, + ), + name: PublicState::new( + context, + 8, + FieldCompressedStringSerializationMethods, + ), + decimals: PublicState::new( + context, + 9, + U8SerializationMethods, + ), } } } @@ -124,9 +144,15 @@ contract Token { // docs:start:constructor #[aztec(private)] - fn constructor(admin: AztecAddress) { - let selector = FunctionSelector::from_signature("_initialize((Field))"); - context.call_public_function(context.this_address(), selector, [admin.to_field()]); + fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { + let selector = FunctionSelector::from_signature("_initialize((Field),(Field),(Field),u8)"); + let name_s = FieldCompressedString::from_string(name); + let symbol_s = FieldCompressedString::from_string(symbol); + context.call_public_function( + context.this_address(), + selector, + [admin.to_field(), name_s.serialize()[0], symbol_s.serialize()[0], decimals as Field] + ); } // docs:end:constructor @@ -140,6 +166,33 @@ contract Token { } // docs:end:set_admin + #[aztec(public)] + fn public_get_name() -> pub FieldCompressedString { + storage.name.read() + } + + unconstrained fn un_get_name() -> pub [u8; 31] { + storage.name.read().to_bytes() + } + + #[aztec(public)] + fn public_get_symbol() -> pub FieldCompressedString { + storage.symbol.read() + } + + unconstrained fn un_get_symbol() -> pub [u8; 31] { + storage.symbol.read().to_bytes() + } + + #[aztec(public)] + fn public_get_decimals() -> pub u8 { + storage.decimals.read() + } + + unconstrained fn un_get_decimals() -> pub u8 { + storage.decimals.read() + } + // docs:start:set_minter #[aztec(public)] fn set_minter(minter: AztecAddress, approve: bool) { @@ -312,10 +365,18 @@ contract Token { // docs:start:initialize #[aztec(public)] - internal fn _initialize(new_admin: AztecAddress) { + internal fn _initialize( + new_admin: AztecAddress, + name: FieldCompressedString, + symbol: FieldCompressedString, + decimals: u8 + ) { assert(!new_admin.is_zero(), "invalid admin"); storage.admin.write(new_admin); storage.minters.at(new_admin.to_field()).write(true); + storage.name.write(name); + storage.symbol.write(symbol); + storage.decimals.write(decimals); } // docs:end:initialize