Skip to content

Commit

Permalink
feat: pay fees and get refund privately
Browse files Browse the repository at this point in the history
  • Loading branch information
alexghr committed Mar 4, 2024
1 parent 9762dbe commit e14d356
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,46 @@ impl Token {
Self { address }
}

pub fn transfer_public(
pub fn split_into_partial_notes_pair(
self: Self,
context: PublicContext,
context: &mut PrivateContext,
from: AztecAddress,
to: AztecAddress,
amount: Field,
nonce: Field
) -> [Field; RETURN_VALUES_LENGTH] {
context.call_private_function(
self.address,
FunctionSelector::from_signature("split_into_partial_notes_pair((Field),(Field),Field,Field)"),
[from.to_field(), to.to_field(), amount, nonce]
)
}

pub fn complete_partial_notes_pair(
self: Self,
context: &mut PublicContext,
partial_note_hashes: [Field; 2],
amounts: [Field; 2]
) {
let _ = context.call_public_function(
self.address,
FunctionSelector::from_signature("transfer_public((Field),(Field),Field,Field)"),
[from.to_field(), to.to_field(), amount, nonce]
FunctionSelector::from_signature("complete_partial_notes_pair([Field;2],[Field;2])"),
[partial_note_hashes[0], partial_note_hashes[1], amounts[0], amounts[1]]
);
}

// Private
pub fn unshield(
pub fn transfer_public(
self: Self,
context: &mut PrivateContext,
context: PublicContext,
from: AztecAddress,
to: AztecAddress,
amount: Field,
nonce: Field
) -> [Field; RETURN_VALUES_LENGTH] {
context.call_private_function(
) {
let _ = context.call_public_function(
self.address,
FunctionSelector::from_signature("unshield((Field),(Field),Field,Field)"),
FunctionSelector::from_signature("transfer_public((Field),(Field),Field,Field)"),
[from.to_field(), to.to_field(), amount, nonce]
)
);
}
}
17 changes: 14 additions & 3 deletions noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ contract FPC {
fn fee_entrypoint_private(amount: Field, asset: AztecAddress, nonce: Field) {
assert(asset == storage.other_asset.read_private());

let _res = Token::at(asset).unshield(
let partial_note_hashes = Token::at(asset).split_into_partial_notes_pair(
&mut context,
context.msg_sender(),
context.this_address(),
Expand All @@ -42,11 +42,22 @@ contract FPC {

let _void = context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("pay_fee((Field),Field,(Field))"),
[context.msg_sender().to_field(), amount, asset.to_field()]
FunctionSelector::from_signature("pay_fee_with_partial_notes(Field,(Field),[Field;2])"),
[amount, asset.to_field(), partial_note_hashes[0], partial_note_hashes[1]]
);
}

#[aztec(public)]
internal fn pay_fee_with_partial_notes(amount: Field, asset: AztecAddress, partial_note_hashes: [Field; 2]) {
let refund = context.call_public_function(
storage.fee_asset.read_public(),
FunctionSelector::from_signature("pay_fee(Field)"),
[amount]
)[0];

Token::at(asset).complete_partial_notes_pair(&mut context, partial_note_hashes, [refund, amount - refund])
}

#[aztec(private)]
fn fee_entrypoint_public(amount: Field, asset: AztecAddress, nonce: Field) {
let _void = context.call_public_function(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ contract Token {
// docs:end:balance_of_public

#[aztec(private)]
fn split_to_partial_notes(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> [Field; 2] {
fn split_into_partial_notes_pair(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> [Field; 2] {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
Expand All @@ -411,7 +411,7 @@ contract Token {

context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("auth_partial_notes((Field))"),
FunctionSelector::from_signature("auth_partial_notes_pair((Field))"),
[hash.to_field()]
);

Expand All @@ -422,12 +422,12 @@ contract Token {
}

#[aztec(public)]
internal fn auth_partial_notes(hash: PartialNotesHash) {
internal fn auth_partial_notes_pair(hash: PartialNotesHash) {
storage.partial_notes.at(hash).write(true);
}

#[aztec(public)]
fn complete_partial_notes(partial_note_hashes: [Field; 2], amounts: [Field; 2]) {
fn complete_partial_notes_pair(partial_note_hashes: [Field; 2], amounts: [Field; 2]) {
let hash = compute_partial_notes_pair_hash(
partial_note_hashes[0],
partial_note_hashes[1],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class PrivateFeePaymentMethod implements FeePaymentMethod {
const messageHash = computeAuthWitMessageHash(this.paymentContract, {
args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce],
functionData: new FunctionData(
FunctionSelector.fromSignature('unshield((Field),(Field),Field,Field)'),
FunctionSelector.fromSignature('split_into_partial_notes_pair((Field),(Field),Field,Field)'),
false,
true,
false,
Expand Down
50 changes: 46 additions & 4 deletions yarn-project/end-to-end/src/e2e_fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import {
ExtendedNote,
Fr,
FunctionSelector,
GrumpkinScalar,
Note,
PrivateFeePaymentMethod,
TxHash,
computeMessageSecretHash,
generatePublicKey,
} from '@aztec/aztec.js';
import { decodeFunctionSignature } from '@aztec/foundation/abi';
import { BufferReader } from '@aztec/foundation/serialize';
import { TokenContract as BananaCoin, FPCContract, GasTokenContract } from '@aztec/noir-contracts.js';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -88,9 +91,18 @@ describe('e2e_fees', () => {

e2eContext.logger(`BananaCoin deployed at ${bananaCoin.address}`);

bananaFPC = await FPCContract.deploy(e2eContext.wallets[0], bananaCoin.address, gasTokenContract.address)
const fpcPrivateKey = GrumpkinScalar.random();
bananaFPC = await FPCContract.deployWithPublicKey(
generatePublicKey(fpcPrivateKey),
e2eContext.wallets[0],
bananaCoin.address,
gasTokenContract.address,
)
.send()
.deployed();

await e2eContext.pxe.registerAccount(fpcPrivateKey, bananaFPC.partialAddress);

e2eContext.logger(`bananaPay deployed at ${bananaFPC.address}`);
await gasBridgeTestHarness.bridgeFromL1ToL2(InitialFPCGas + 1n, InitialFPCGas, bananaFPC.address);

Expand Down Expand Up @@ -153,7 +165,7 @@ describe('e2e_fees', () => {
* increase alice BC.public by RefundAmount
*
*/
await bananaCoin.methods
const tx = await bananaCoin.methods
.mint_public(aliceAddress, MintedBananasAmount)
.send({
fee: {
Expand All @@ -163,15 +175,17 @@ describe('e2e_fees', () => {
})
.wait();

await completePartialTokenNotes([RefundAmount, FeeAmount], tx.txHash);

await expectMapping(
bananaPrivateBalances,
[aliceAddress, bananaFPC.address, sequencerAddress],
[PrivateInitialBananasAmount - MaxFee, 0n, 0n],
[PrivateInitialBananasAmount - FeeAmount, FeeAmount, 0n],
);
await expectMapping(
bananaPublicBalances,
[aliceAddress, bananaFPC.address, sequencerAddress],
[MintedBananasAmount + RefundAmount, MaxFee - RefundAmount, 0n],
[MintedBananasAmount, 0n, 0n],
);
await expectMapping(
gasBalances,
Expand All @@ -195,4 +209,32 @@ describe('e2e_fees', () => {
);
await e2eContext.wallets[accountIndex].addNote(extendedNote);
};

const completePartialTokenNotes = async (expectedAmounts: (bigint | number)[], completionTxHash: TxHash) => {
// TODO use a dedicated event selector for these completion events once implemented
const logs = await e2eContext.pxe.getUnencryptedLogs({ txHash: completionTxHash });
// logs[0] contains the partial note hashes created in private
const completedEvent0 = BufferReader.asReader(logs.logs[1].log.data).readArray(2, Fr);
const completedEvent1 = BufferReader.asReader(logs.logs[2].log.data).readArray(2, Fr);

// constant value taken from the Noir contract
const tokenNoteId = new Fr(8411110710111078111116101n);

expect(completedEvent0).toEqual([tokenNoteId, new Fr(expectedAmounts[0])]);
expect(completedEvent1).toEqual([tokenNoteId, new Fr(expectedAmounts[1])]);

await e2eContext.pxe.completePartialNote(
bananaCoin.address,
tokenNoteId,
[[0, new Fr(expectedAmounts[0])]],
completionTxHash,
);

await e2eContext.pxe.completePartialNote(
bananaCoin.address,
tokenNoteId,
[[0, new Fr(expectedAmounts[1])]],
completionTxHash,
);
};
});
12 changes: 6 additions & 6 deletions yarn-project/end-to-end/src/e2e_partial_token_notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('e2e_partial_token_notes', () => {
await expect(tokenContract.methods.balance_of_private(ctx.wallets[0].getAddress()).view()).resolves.toEqual(0n);

const completeNotesTx = await tokenContract.methods
.complete_partial_notes(partialNoteHashes, [400, 600])
.complete_partial_notes_pair(partialNoteHashes, [400, 600])
.send()
.wait();

Expand Down Expand Up @@ -71,17 +71,17 @@ describe('e2e_partial_token_notes', () => {

it('partial notes are completable only once', async () => {
const partialNoteHashes = await createPartialNotes(10n);
await tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 5n]).send().wait();
await tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n]).send().wait();
await expect(
tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 5n]).send().wait(),
tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n]).send().wait(),
).rejects.toThrow(/was dropped/);
});

it('partial notes are be constrained to original total amount', async () => {
const partialNoteHashes = await createPartialNotes(10n);

await expect(
tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 6n]).send().wait(),
tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 6n]).send().wait(),
).rejects.toThrow(/Partial notes not authorized/);
});

Expand All @@ -91,15 +91,15 @@ describe('e2e_partial_token_notes', () => {
await expect(
tokenContract
.withWallet(ctx.wallets[1])
.methods.complete_partial_notes(partialNoteHashes, [5n, 5n])
.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n])
.send()
.wait(),
).rejects.toThrow(/Partial notes not authorized/);
});

const createPartialNotes = async (totalAmount: bigint | number) => {
const { txHash } = await tokenContract.methods
.split_to_partial_notes(ctx.wallets[0].getAddress(), ctx.wallets[1].getAddress(), totalAmount, 0)
.split_into_partial_notes_pair(ctx.wallets[0].getAddress(), ctx.wallets[1].getAddress(), totalAmount, 0)
.send()
.wait();

Expand Down

0 comments on commit e14d356

Please sign in to comment.