diff --git a/.circleci/config.yml b/.circleci/config.yml index a0719f733e1..d928a7af0e2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -786,6 +786,18 @@ jobs: aztec_manifest_key: end-to-end <<: *defaults_e2e_test + e2e-crowdfunding-and-claim: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_crowdfunding_and_claim.test.ts + aztec_manifest_key: end-to-end + e2e-public-cross-chain-messaging: steps: - *checkout @@ -1377,6 +1389,7 @@ workflows: - e2e-multiple-accounts-1-enc-key: *e2e_test - e2e-cli: *e2e_test - e2e-cross-chain-messaging: *e2e_test + - e2e-crowdfunding-and-claim: *e2e_test - e2e-public-cross-chain-messaging: *e2e_test - e2e-public-to-private-messaging: *e2e_test - e2e-account-contracts: *e2e_test @@ -1440,6 +1453,7 @@ workflows: - e2e-multiple-accounts-1-enc-key - e2e-cli - e2e-cross-chain-messaging + - e2e-crowdfunding-and-claim - e2e-public-cross-chain-messaging - e2e-public-to-private-messaging - e2e-account-contracts diff --git a/noir-projects/aztec-nr/aztec/src/note/note_header.nr b/noir-projects/aztec-nr/aztec/src/note/note_header.nr index b807deea79a..27f81945c6a 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_header.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_header.nr @@ -1,5 +1,5 @@ use dep::protocol_types::address::AztecAddress; -use dep::protocol_types::traits::Empty; +use dep::protocol_types::traits::{Empty, Serialize}; struct NoteHeader { contract_address: AztecAddress, @@ -21,3 +21,9 @@ impl NoteHeader { NoteHeader { contract_address, nonce, storage_slot, is_transient: false } } } + +impl Serialize<4> for NoteHeader { + fn serialize(self) -> [Field; 4] { + [self.contract_address.to_field(), self.nonce, self.storage_slot, self.is_transient as Field] + } +} diff --git a/noir-projects/aztec-nr/value-note/src/value_note.nr b/noir-projects/aztec-nr/value-note/src/value_note.nr index f29769ee3a3..f0d59d31e3e 100644 --- a/noir-projects/aztec-nr/value-note/src/value_note.nr +++ b/noir-projects/aztec-nr/value-note/src/value_note.nr @@ -96,3 +96,11 @@ impl ValueNote { ValueNote { value, owner, randomness, header } } } + +impl Serialize<7> for ValueNote { + fn serialize(self) -> [Field; 7] { + let header = self.header.serialize(); + + [self.value, self.owner.to_field(), self.randomness, header[0], header[1], header[2], header[3]] + } +} diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 08b4006f56b..e740f210ac7 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -6,9 +6,11 @@ members = [ "contracts/benchmarking_contract", "contracts/card_game_contract", "contracts/child_contract", + "contracts/claim_contract", "contracts/contract_class_registerer_contract", "contracts/contract_instance_deployer_contract", "contracts/counter_contract", + "contracts/crowdfunding_contract", "contracts/delegator_contract", "contracts/delegated_on_contract", "contracts/docs_example_contract", diff --git a/noir-projects/noir-contracts/contracts/claim_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/claim_contract/Nargo.toml new file mode 100644 index 00000000000..cc664c3edb1 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/claim_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "claim_contract" +authors = [""] +compiler_version = ">=0.18.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +value_note = { path = "../../../aztec-nr/value-note" } diff --git a/noir-projects/noir-contracts/contracts/claim_contract/src/interfaces.nr b/noir-projects/noir-contracts/contracts/claim_contract/src/interfaces.nr new file mode 100644 index 00000000000..187608f6ec8 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/claim_contract/src/interfaces.nr @@ -0,0 +1,37 @@ +use dep::aztec::{ + protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress}, + context::PrivateContext, +}; + +struct Token { + address: AztecAddress, +} + +impl Token { + pub fn at(address: AztecAddress) -> Self { + Self { address } + } + + fn mint_public(self: Self, context: &mut PrivateContext, to: AztecAddress, amount: Field) { + let _ret = context.call_public_function( + self.address, + FunctionSelector::from_signature("mint_public((Field),Field)"), + [to.to_field(), amount] + ); + } + + pub fn transfer( + self: Self, + context: &mut PrivateContext, + from: AztecAddress, + to: AztecAddress, + amount: Field, + nonce: Field + ) { + let _ret = context.call_private_function( + self.address, + FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"), + [from.to_field(), to.to_field(), amount, nonce] + ); + } +} diff --git a/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr new file mode 100644 index 00000000000..12a9c69f1d0 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/claim_contract/src/main.nr @@ -0,0 +1,59 @@ +contract Claim { + mod interfaces; + + use dep::aztec::{ + history::note_inclusion::prove_note_inclusion, + protocol_types::{ + abis::function_selector::FunctionSelector, + address::AztecAddress, + }, + state_vars::SharedImmutable, + }; + use dep::value_note::value_note::ValueNote; + use interfaces::Token; + + struct Storage { + // Address of a contract based on whose notes we distribute the rewards + target_contract: SharedImmutable, + // Token to be distributed as a reward when claiming + reward_token: SharedImmutable, + } + + #[aztec(private)] + fn constructor(target_contract: AztecAddress, reward_token: AztecAddress) { + let selector = FunctionSelector::from_signature("_initialize((Field),(Field))"); + context.call_public_function( + context.this_address(), + selector, + [target_contract.to_field(), reward_token.to_field()] + ); + } + + #[aztec(public)] + #[aztec(internal)] + #[aztec(noinitcheck)] + fn _initialize(target_contract: AztecAddress, reward_token: AztecAddress) { + storage.target_contract.initialize(target_contract); + storage.reward_token.initialize(reward_token); + } + + #[aztec(private)] + fn claim(proof_note: ValueNote) { + // 1) Check that the note corresponds to the target contract + let target_address = storage.target_contract.read_private(); + assert(target_address == proof_note.header.contract_address, "Note does not correspond to the target contract"); + + // 2) Prove that the note hash exists in the note hash tree + prove_note_inclusion(proof_note, context); + + // 3) Compute and emit a nullifier which is unique to the note and this contract to ensure the reward can be + // claimed only once with the given note. + // Note: The nullifier is unique to the note and THIS contract because the protocol siloes all nullifiers with + // the address of a contract it was emitted from. + context.push_new_nullifier(proof_note.compute_nullifier(&mut context), 0); + + // 4) Finally we mint the reward token to the sender of the transaction + let reward_token = Token::at(storage.reward_token.read_private()); + reward_token.mint_public(&mut context, context.msg_sender(), proof_note.value); + } +} diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml new file mode 100644 index 00000000000..2003f03fdd9 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "crowdfunding_contract" +authors = [""] +compiler_version = ">=0.18.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +value_note = { path = "../../../aztec-nr/value-note" } diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/interfaces.nr b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/interfaces.nr new file mode 100644 index 00000000000..746f8e0e24c --- /dev/null +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/interfaces.nr @@ -0,0 +1,27 @@ +use dep::aztec::protocol_types::{abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}}; +use dep::aztec::{context::{PrivateContext, PublicContext}}; + +struct Token { + address: AztecAddress, +} + +impl Token { + pub fn at(address: AztecAddress) -> Self { + Self { address } + } + + pub fn transfer( + self: Self, + context: &mut PrivateContext, + from: AztecAddress, + to: AztecAddress, + amount: Field, + nonce: Field + ) { + let _ret = context.call_private_function( + self.address, + FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"), + [from.to_field(), to.to_field(), amount, nonce] + ); + } +} diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr new file mode 100644 index 00000000000..7b406dadb10 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr @@ -0,0 +1,108 @@ +contract Crowdfunding { + mod interfaces; + + use dep::aztec::{ + log::emit_unencrypted_log_from_private, + protocol_types::{ + abis::function_selector::FunctionSelector, + address::AztecAddress, + traits::Serialize + }, + state_vars::{PrivateSet, PublicImmutable, SharedImmutable}, + }; + use dep::value_note::value_note::ValueNote; + use interfaces::Token; + + #[event] + struct WithdrawalProcessed { + who: AztecAddress, + amount: u64, + } + + impl Serialize<2> for WithdrawalProcessed { + fn serialize(self: Self) -> [Field; 2] { + [self.who.to_field(), self.amount as Field] + } + } + + struct Storage { + // Token used for donations (e.g. DAI) + donation_token: SharedImmutable, + // Crowdfunding campaign operator + operator: SharedImmutable, + // End of the crowdfunding campaign after which no more donations are accepted + // TODO(#4990): Make deadline a u64 once the neccessary traits are implemented + deadline: PublicImmutable, + // Notes emitted to donors when they donate (later on used to claim rewards in the Claim contract) + claim_notes: PrivateSet, + } + + #[aztec(private)] + fn constructor(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) { + let selector = FunctionSelector::from_signature("_initialize((Field),(Field),Field)"); + context.call_public_function( + context.this_address(), + selector, + [donation_token.to_field(), operator.to_field(), deadline as Field] + ); + } + + #[aztec(public)] + #[aztec(internal)] + #[aztec(noinitcheck)] + // TODO(#4990): Make deadline a u64 once the neccessary traits are implemented + fn _initialize(donation_token: AztecAddress, operator: AztecAddress, deadline: Field) { + storage.donation_token.initialize(donation_token); + storage.operator.initialize(operator); + storage.deadline.initialize(deadline); + } + + #[aztec(public)] + #[aztec(internal)] + fn _check_deadline() { + // TODO(#4990): Remove the cast here once u64 is used directly + let deadline = storage.deadline.read() as u64; + assert(context.timestamp() as u64 < deadline, "Deadline has passed"); + } + + #[aztec(private)] + fn donate(amount: u64) { + // 1) Check that the deadline has not passed + context.call_public_function( + context.this_address(), + FunctionSelector::from_signature("_check_deadline()"), + [] + ); + + // 2) Transfer the donation tokens from donor to this contract + let donation_token = Token::at(storage.donation_token.read_private()); + donation_token.transfer( + &mut context, + context.msg_sender(), + context.this_address(), + amount as Field, + 0 + ); + + // 3) Create a value note for the donor so that he can later on claim a rewards token in the Claim + // contract by proving that the hash of this note exists in the note hash tree. + let mut note = ValueNote::new(amount as Field, context.msg_sender()); + storage.claim_notes.insert(&mut note, true); + } + + // Withdraws balance to the operator. Requires that msg_sender() is the operator. + #[aztec(private)] + fn withdraw(amount: u64) { + // 1) Check that msg_sender() is the operator + let operator_address = storage.operator.read_private(); + assert(context.msg_sender() == operator_address, "Not an operator"); + + // 2) Transfer the donation tokens from this contract to the operator + let donation_token = Token::at(storage.donation_token.read_private()); + donation_token.transfer(&mut context, context.this_address(), operator_address, amount as Field, 0); + + // 3) Emit an unencrypted event so that anyone can audit how much the operator has withdrawn + let event = WithdrawalProcessed { amount, who: operator_address }; + emit_unencrypted_log_from_private(&mut context, event.serialize()); + } +} diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index b84b8371181..832987a741a 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -84,6 +84,10 @@ export abstract class BaseWallet implements Wallet { getNotes(filter: NoteFilter): Promise { return this.pxe.getNotes(filter); } + // TODO(#4956): Un-expose this + getNoteNonces(note: ExtendedNote): Promise { + return this.pxe.getNoteNonces(note); + } getPublicStorageAt(contract: AztecAddress, storageSlot: Fr): Promise { return this.pxe.getPublicStorageAt(contract, storageSlot); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index e32101b0971..62f07cfdbae 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -171,6 +171,15 @@ export interface PXE { */ getNotes(filter: NoteFilter): Promise; + /** + * Finds the nonce(s) for a given note. + * @param note - The note to find the nonces for. + * @returns The nonces of the note. + * @remarks More than a single nonce may be returned since there might be more than one nonce for a given note. + * TODO(#4956): Un-expose this + */ + getNoteNonces(note: ExtendedNote): Promise; + /** * Adds a note to the database. * @throws If the note hash of the note doesn't exist in the tree. 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 4eabfd385f4..e79fef0116c 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 @@ -100,9 +100,11 @@ AppSubscriptionContractArtifact BenchmarkingContractArtifact CardGameContractArtifact ChildContractArtifact +ClaimContractArtifact ContractClassRegistererContractArtifact ContractInstanceDeployerContractArtifact CounterContractArtifact +CrowdfundingContractArtifact DelegatedOnContractArtifact DelegatorContractArtifact DocsExampleContractArtifact diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts new file mode 100644 index 00000000000..26a4c06cea3 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -0,0 +1,320 @@ +import { + AccountWallet, + AztecAddress, + CheatCodes, + DebugLogger, + ExtendedNote, + Fr, + GrumpkinScalar, + Note, + PXE, + TxHash, + computeAuthWitMessageHash, + computeMessageSecretHash, + generatePublicKey, + getContractInstanceFromDeployParams, +} from '@aztec/aztec.js'; +import { EthAddress, computePartialAddress } from '@aztec/circuits.js'; +import { InclusionProofsContract } from '@aztec/noir-contracts.js'; +import { ClaimContract } from '@aztec/noir-contracts.js/Claim'; +import { CrowdfundingContract, CrowdfundingContractArtifact } from '@aztec/noir-contracts.js/Crowdfunding'; +import { TokenContract } from '@aztec/noir-contracts.js/Token'; + +import { jest } from '@jest/globals'; + +import { setup } from './fixtures/utils.js'; + +jest.setTimeout(200_000); + +// Tests crowdfunding via the Crowdfunding contract and claiming the reward token via the Claim contract +describe('e2e_crowdfunding_and_claim', () => { + const donationTokenMetadata = { + name: 'Donation Token', + symbol: 'DNT', + decimals: 18n, + }; + + const rewardTokenMetadata = { + name: 'Reward Token', + symbol: 'RWT', + decimals: 18n, + }; + + let teardown: () => Promise; + let operatorWallet: AccountWallet; + let donorWallets: AccountWallet[]; + let wallets: AccountWallet[]; + let logger: DebugLogger; + + let donationToken: TokenContract; + let rewardToken: TokenContract; + let crowdfundingContract: CrowdfundingContract; + let claimContract: ClaimContract; + + let crowdfundingPrivateKey; + let crowdfundingPublicKey; + let pxe: PXE; + let cheatCodes: CheatCodes; + let deadline: number; // end of crowdfunding period + + let valueNote!: any; + + const addPendingShieldNoteToPXE = async ( + wallet: AccountWallet, + amount: bigint, + secretHash: Fr, + txHash: TxHash, + address: AztecAddress, + ) => { + 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, wallet.getAddress(), address, storageSlot, noteTypeId, txHash); + await wallet.addNote(extendedNote); + }; + + beforeAll(async () => { + ({ cheatCodes, teardown, logger, pxe, wallets } = await setup(3)); + operatorWallet = wallets[0]; + donorWallets = wallets.slice(1); + + // We set the deadline to a week from now + deadline = (await cheatCodes.eth.timestamp()) + 7 * 24 * 60 * 60; + + donationToken = await TokenContract.deploy( + operatorWallet, + operatorWallet.getAddress(), + donationTokenMetadata.name, + donationTokenMetadata.symbol, + donationTokenMetadata.decimals, + ) + .send() + .deployed(); + logger(`Donation Token deployed to ${donationToken.address}`); + + rewardToken = await TokenContract.deploy( + operatorWallet, + operatorWallet.getAddress(), + rewardTokenMetadata.name, + rewardTokenMetadata.symbol, + rewardTokenMetadata.decimals, + ) + .send() + .deployed(); + logger(`Reward Token deployed to ${rewardToken.address}`); + + crowdfundingPrivateKey = GrumpkinScalar.random(); + crowdfundingPublicKey = generatePublicKey(crowdfundingPrivateKey); + const salt = Fr.random(); + + const args = [donationToken.address, operatorWallet.getAddress(), deadline]; + const deployInfo = getContractInstanceFromDeployParams( + CrowdfundingContractArtifact, + args, + salt, + crowdfundingPublicKey, + EthAddress.ZERO, + ); + await pxe.registerAccount(crowdfundingPrivateKey, computePartialAddress(deployInfo)); + + crowdfundingContract = await CrowdfundingContract.deployWithPublicKey( + crowdfundingPublicKey, + operatorWallet, + donationToken.address, + operatorWallet.getAddress(), + deadline, + ) + .send({ contractAddressSalt: salt }) + .deployed(); + logger(`Crowdfunding contract deployed at ${crowdfundingContract.address}`); + + claimContract = await ClaimContract.deploy(operatorWallet, crowdfundingContract.address, rewardToken.address) + .send() + .deployed(); + logger(`Claim contract deployed at ${claimContract.address}`); + + await rewardToken.methods.set_minter(claimContract.address, true).send().wait(); + + await mintDNTToDonors(); + }); + + afterAll(() => teardown()); + + const mintDNTToDonors = async () => { + const secret = Fr.random(); + const secretHash = computeMessageSecretHash(secret); + + const [txReceipt1, txReceipt2] = await Promise.all([ + donationToken.withWallet(operatorWallet).methods.mint_private(1234n, secretHash).send().wait(), + donationToken.withWallet(operatorWallet).methods.mint_private(2345n, secretHash).send().wait(), + ]); + + await addPendingShieldNoteToPXE( + donorWallets[0], + 1234n, + secretHash, + txReceipt1.txHash, + donationToken.withWallet(operatorWallet).address, + ); + await addPendingShieldNoteToPXE( + donorWallets[1], + 2345n, + secretHash, + txReceipt2.txHash, + donationToken.withWallet(operatorWallet).address, + ); + + await Promise.all([ + donationToken + .withWallet(donorWallets[0]) + .methods.redeem_shield(donorWallets[0].getAddress(), 1234n, secret) + .send() + .wait(), + donationToken + .withWallet(donorWallets[1]) + .methods.redeem_shield(donorWallets[1].getAddress(), 2345n, secret) + .send() + .wait(), + ]); + }; + + // Processes extended note such that it can be passed to a claim function of Claim contract + const processExtendedNote = async (extendedNote: ExtendedNote) => { + // TODO(#4956): Make fetching the nonce manually unnecessary + // To be able to perform the inclusion proof we need to fetch the nonce of the value note + const noteNonces = await pxe.getNoteNonces(extendedNote); + expect(noteNonces?.length).toEqual(1); + + return { + header: { + // eslint-disable-next-line camelcase + contract_address: extendedNote.contractAddress, + // eslint-disable-next-line camelcase + storage_slot: extendedNote.storageSlot, + // eslint-disable-next-line camelcase + is_transient: false, + nonce: noteNonces[0], + }, + value: extendedNote.note.items[0], + owner: extendedNote.note.items[1], + randomness: extendedNote.note.items[2], + }; + }; + + it('full donor flow', async () => { + const donationAmount = 1000n; + + // 1) We add authwit so that the Crowdfunding contract can transfer donor's DNT + { + const action = donationToken + .withWallet(donorWallets[0]) + .methods.transfer(donorWallets[0].getAddress(), crowdfundingContract.address, donationAmount, 0); + const messageHash = computeAuthWitMessageHash(crowdfundingContract.address, action.request()); + const witness = await donorWallets[0].createAuthWitness(messageHash); + await donorWallets[0].addAuthWitness(witness); + } + + // 2) We donate to the crowdfunding contract + { + const donateTxReceipt = await crowdfundingContract + .withWallet(donorWallets[0]) + .methods.donate(donationAmount) + .send() + .wait({ + debug: true, + }); + + // Get the notes emitted by the Crowdfunding contract and check that only 1 was emitted (the value note) + const notes = donateTxReceipt.debugInfo?.visibleNotes.filter(x => + x.contractAddress.equals(crowdfundingContract.address), + ); + expect(notes!.length).toEqual(1); + + // Set the value note in a format which can be passed to claim function + valueNote = await processExtendedNote(notes![0]); + } + + // 3) We claim the reward token via the Claim contract + { + await claimContract.withWallet(donorWallets[0]).methods.claim(valueNote).send().wait(); + } + + // Since the RWT is minted 1:1 with the DNT, the balance of the reward token should be equal to the donation amount + const balanceRWT = await rewardToken.methods.balance_of_public(donorWallets[0].getAddress()).view(); + expect(balanceRWT).toEqual(donationAmount); + + const balanceDNTBeforeWithdrawal = await donationToken.methods + .balance_of_private(operatorWallet.getAddress()) + .view(); + expect(balanceDNTBeforeWithdrawal).toEqual(0n); + + // 4) At last, we withdraw the raised funds from the crowdfunding contract to the operator's address + await crowdfundingContract.methods.withdraw(donationAmount).send().wait(); + + const balanceDNTAfterWithdrawal = await donationToken.methods + .balance_of_private(operatorWallet.getAddress()) + .view(); + + // Operator should have all the DNT now + expect(balanceDNTAfterWithdrawal).toEqual(donationAmount); + }); + + it('cannot claim twice', async () => { + // The first claim was executed in the previous test + await expect(claimContract.withWallet(donorWallets[0]).methods.claim(valueNote).send().wait()).rejects.toThrow(); + }); + + it('cannot claim with a non-existent note', async () => { + // We get a non-existent note by copy the value note and change the randomness to a random value + const nonExistentNote = { ...valueNote }; + nonExistentNote.randomness = Fr.random(); + + await expect( + claimContract.withWallet(donorWallets[0]).methods.claim(nonExistentNote).send().wait(), + ).rejects.toThrow(); + }); + + it('cannot claim with existing note which was not emitted by the crowdfunding contract', async () => { + const owner = wallets[0].getAddress(); + + // 1) Deploy IncludeProofs contract + const inclusionsProofsContract = await InclusionProofsContract.deploy(wallets[0], 0n).send().deployed(); + + // 2) Create a note + let note: any; + { + const receipt = await inclusionsProofsContract.methods.create_note(owner, 5n).send().wait({ debug: true }); + const { visibleNotes } = receipt.debugInfo!; + expect(visibleNotes.length).toEqual(1); + note = await processExtendedNote(visibleNotes![0]); + } + + // 3) Test the note was included + await inclusionsProofsContract.methods.test_note_inclusion(owner, false, 0n, true).send().wait(); + + // 4) Finally, check that the claim process fails + await expect(claimContract.withWallet(donorWallets[0]).methods.claim(note).send().wait()).rejects.toThrow(); + }); + + it('cannot donate after a deadline', async () => { + const donationAmount = 1000n; + + // 1) We add authwit so that the Crowdfunding contract can transfer donor's DNT + { + const action = donationToken + .withWallet(donorWallets[1]) + .methods.transfer(donorWallets[1].getAddress(), crowdfundingContract.address, donationAmount, 0); + const messageHash = computeAuthWitMessageHash(crowdfundingContract.address, action.request()); + const witness = await donorWallets[1].createAuthWitness(messageHash); + await donorWallets[1].addAuthWitness(witness); + } + + // 2) We set next block timestamp to be after the deadline + await cheatCodes.aztec.warp(deadline + 1); + + // 3) We donate to the crowdfunding contract + await expect( + crowdfundingContract.withWallet(donorWallets[1]).methods.donate(donationAmount).send().wait(), + ).rejects.toThrow(); + }); +}); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 7e83afc6e12..661091a8c67 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -333,8 +333,9 @@ export class PXEService implements PXE { * @param note - The note to find the nonces for. * @returns The nonces of the note. * @remarks More than a single nonce may be returned since there might be more than one nonce for a given note. + * TODO(#4956): Un-expose this */ - private async getNoteNonces(note: ExtendedNote): Promise { + public async getNoteNonces(note: ExtendedNote): Promise { const tx = await this.node.getTxEffect(note.txHash); if (!tx) { throw new Error(`Unknown tx: ${note.txHash}`);