Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit 0ab4a8c

Browse files
authored
feat: add js tests (#361)
# 🤖 Linear Closes AZT-XXX ## Description add js tests
1 parent f82399f commit 0ab4a8c

2 files changed

Lines changed: 550 additions & 0 deletions

File tree

src/ts/test/multitoken.test.ts

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
import { Fr } from '@aztec/aztec.js/fields';
2+
import { AztecAddress } from '@aztec/aztec.js/addresses';
3+
import { type EmbeddedWallet } from '@aztec/wallets/embedded';
4+
import { SetPublicAuthwitContractInteraction, lookupValidity } from '@aztec/aztec.js/authorization';
5+
import { type ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization';
6+
7+
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
8+
9+
import {
10+
setupTestSuite,
11+
AMOUNT,
12+
PRIVATE_ADDRESS,
13+
MULTITOKEN_NAME,
14+
MULTITOKEN_SYMBOL,
15+
ID_A,
16+
fieldFromShortString,
17+
compressedStringToBigInt,
18+
deployMultiTokenWithMinter,
19+
initializeMultiTokenTransferCommitment,
20+
expectMultiTokenTransferEvents,
21+
} from './utils.js';
22+
23+
import { MultiTokenContract } from '../../../src/artifacts/MultiToken.js';
24+
25+
const TEST_TIMEOUT = 300_000;
26+
27+
// Scope of this suite: ONE end-to-end happy-path per main (state-mutating) function, exercised through
28+
// the real client stack (deploy -> call -> read via SDK, with real authwit + additionalScopes), asserting
29+
// the resulting balances and the decoded event. It answers "does each function work end-to-end?".
30+
// Correctness details — reverts, overflow/underflow, per-id isolation, id=0, note lifecycle, the ARC-403
31+
// authorization hook, boundaries — are covered by the Noir/TXE suite (src/multitoken_contract/src/test/*).
32+
describe('MultiToken', () => {
33+
let cleanup: () => Promise<void>;
34+
35+
let wallet: EmbeddedWallet;
36+
let accounts: AztecAddress[];
37+
38+
let alice: AztecAddress;
39+
let bob: AztecAddress;
40+
let carl: AztecAddress;
41+
42+
let multitoken: MultiTokenContract;
43+
44+
beforeAll(async () => {
45+
({ cleanup, wallet, accounts } = await setupTestSuite());
46+
47+
[alice, bob, carl] = accounts;
48+
});
49+
50+
beforeEach(async () => {
51+
// Default deploy: minter = alice, auth_contract = ZERO (ARC-403 hook disabled). The hook's behavior
52+
// is covered exhaustively by the Noir suite (authorization.nr); here every path runs with it off.
53+
multitoken = await deployMultiTokenWithMinter(wallet, alice, alice, AztecAddress.ZERO);
54+
});
55+
56+
afterAll(async () => {
57+
await cleanup();
58+
});
59+
60+
// --- deploy / views ---
61+
62+
it(
63+
'deploys with a minter and reads back name/symbol/minter/auth_contract',
64+
async () => {
65+
const nameField = fieldFromShortString(MULTITOKEN_NAME);
66+
const symbolField = fieldFromShortString(MULTITOKEN_SYMBOL);
67+
68+
const mt = await deployMultiTokenWithMinter(wallet, alice, alice, AztecAddress.ZERO);
69+
70+
const nameResult = (await mt.methods.name().simulate({ from: alice })).result;
71+
const symbolResult = (await mt.methods.symbol().simulate({ from: alice })).result;
72+
expect(compressedStringToBigInt(nameResult)).toBe(nameField.toBigInt());
73+
expect(compressedStringToBigInt(symbolResult)).toBe(symbolField.toBigInt());
74+
75+
const minter = (await mt.methods.get_minter().simulate({ from: alice })).result;
76+
expect(minter.equals(alice)).toBe(true);
77+
78+
const authContract = (await mt.methods.get_auth_contract().simulate({ from: alice })).result;
79+
expect(authContract.equals(AztecAddress.ZERO)).toBe(true);
80+
},
81+
TEST_TIMEOUT,
82+
);
83+
84+
// --- mint_to_private ---
85+
86+
it(
87+
'mints to a private recipient balance and leaks nothing publicly',
88+
async () => {
89+
const id = ID_A;
90+
const { receipt: mintTx } = await multitoken.methods.mint_to_private(bob, id, AMOUNT).send({ from: alice });
91+
// mint_to_private: (no public events)
92+
await expectMultiTokenTransferEvents(mintTx.txHash, multitoken.address, []);
93+
// recipient sees its private balance; no public balance was written.
94+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(AMOUNT);
95+
expect((await multitoken.methods.balance_of_public(bob, id).simulate({ from: bob })).result).toBe(0n);
96+
// an uninvolved third party never observes another account's private notes.
97+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: carl })).result).toBe(0n);
98+
},
99+
TEST_TIMEOUT,
100+
);
101+
102+
// --- mint_to_public ---
103+
104+
it(
105+
'mints to public and emits a 4-field TransferSingle event',
106+
async () => {
107+
const id = ID_A;
108+
109+
const { receipt: mintTx } = await multitoken.methods.mint_to_public(alice, id, AMOUNT).send({ from: alice });
110+
111+
// mint_to_public: TransferSingle(0x0, alice, id, AMOUNT)
112+
await expectMultiTokenTransferEvents(mintTx.txHash, multitoken.address, [
113+
{ from: AztecAddress.ZERO, to: alice, id, amount: AMOUNT },
114+
]);
115+
116+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: alice })).result).toBe(AMOUNT);
117+
// No private note written for the public mint.
118+
expect((await multitoken.methods.balance_of_private(alice, id).simulate({ from: alice })).result).toBe(0n);
119+
},
120+
TEST_TIMEOUT,
121+
);
122+
123+
// --- mint_to_commitment ---
124+
125+
it(
126+
'mints to a recipient-prepared commitment and credits the recipient privately',
127+
async () => {
128+
const id = ID_A;
129+
130+
// Recipient (bob) prepares the commitment for himself in his own settled tx, bound to the minter (alice) as completer.
131+
const commitment = await initializeMultiTokenTransferCommitment(multitoken, bob, bob, alice);
132+
133+
// Minter (alice = msg_sender) completes the commitment by minting into it.
134+
const { receipt: mintTx } = await multitoken.methods
135+
.mint_to_commitment(id, commitment, AMOUNT)
136+
.send({ from: alice });
137+
138+
// mint_to_commitment: TransferSingle(0x0, PRIVATE, id, AMOUNT)
139+
await expectMultiTokenTransferEvents(mintTx.txHash, multitoken.address, [
140+
{ from: AztecAddress.ZERO, to: PRIVATE_ADDRESS, id, amount: AMOUNT },
141+
]);
142+
143+
// bob holds the tokens privately for this id.
144+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(AMOUNT);
145+
},
146+
TEST_TIMEOUT,
147+
);
148+
149+
// --- transfer_public_to_public (happy-path carries the PUBLIC authwit demo) ---
150+
151+
it(
152+
'transfers public to public with a public authwit',
153+
async () => {
154+
const id = ID_A;
155+
156+
const { receipt: mintTx } = await multitoken
157+
.withWallet(wallet)
158+
.methods.mint_to_public(alice, id, AMOUNT)
159+
.send({ from: alice });
160+
161+
// mint_to_public: TransferSingle(0x0, alice, id, AMOUNT)
162+
await expectMultiTokenTransferEvents(mintTx.txHash, multitoken.address, [
163+
{ from: AztecAddress.ZERO, to: alice, id, amount: AMOUNT },
164+
]);
165+
166+
// Build the transfer; carl will submit it under alice's public authwit.
167+
const nonce = Fr.random();
168+
const action = multitoken.withWallet(wallet).methods.transfer_public_to_public(alice, bob, id, AMOUNT, nonce);
169+
170+
const intent: ContractFunctionInteractionCallIntent = {
171+
caller: carl,
172+
action,
173+
};
174+
const authWitness = await wallet.createAuthWit(alice, {
175+
caller: intent.caller,
176+
call: await intent.action.getFunctionCall(),
177+
});
178+
const setPublicAuthwitInteraction = await SetPublicAuthwitContractInteraction.create(wallet, alice, intent, true);
179+
await setPublicAuthwitInteraction.send();
180+
181+
const validity = await lookupValidity(wallet, alice, intent, authWitness);
182+
expect(validity.isValidInPrivate).toBeTruthy();
183+
expect(validity.isValidInPublic).toBeTruthy();
184+
185+
const { receipt: transferTx } = await action.send({ from: carl, authWitnesses: [authWitness] });
186+
187+
// transfer_public_to_public: TransferSingle(alice, bob, id, AMOUNT)
188+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, [
189+
{ from: alice, to: bob, id, amount: AMOUNT },
190+
]);
191+
192+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: carl })).result).toBe(0n);
193+
expect((await multitoken.methods.balance_of_public(bob, id).simulate({ from: carl })).result).toBe(AMOUNT);
194+
},
195+
TEST_TIMEOUT,
196+
);
197+
198+
// --- transfer_public_to_private (happy-path carries the PRIVATE authwit + additionalScopes demo) ---
199+
200+
it(
201+
'transfers public to private under a third-party private authwit with additionalScopes',
202+
async () => {
203+
const id = ID_A;
204+
205+
await multitoken.methods.mint_to_public(alice, id, AMOUNT).send({ from: alice });
206+
207+
// Third party (bob) submits alice -> carl spending alice's PUBLIC balance; gate is a private authwit by alice.
208+
const action = multitoken.methods.transfer_public_to_private(alice, carl, id, AMOUNT, 1n);
209+
210+
const intent: ContractFunctionInteractionCallIntent = {
211+
caller: bob,
212+
action,
213+
};
214+
const witness = await wallet.createAuthWit(alice, {
215+
caller: intent.caller,
216+
call: await intent.action.getFunctionCall(),
217+
});
218+
219+
// additionalScopes includes alice so bob's PXE can read alice's account-contract notes
220+
// (signing key) needed for private authwit verification.
221+
const { receipt: transferTx } = await action.send({
222+
from: bob,
223+
authWitnesses: [witness],
224+
additionalScopes: [alice],
225+
});
226+
227+
// transfer_public_to_private: TransferSingle(alice, PRIVATE, id, AMOUNT) (hidden recipient side is the sentinel)
228+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, [
229+
{ from: alice, to: PRIVATE_ADDRESS, id, amount: AMOUNT },
230+
]);
231+
232+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: bob })).result).toBe(0n);
233+
expect((await multitoken.methods.balance_of_private(carl, id).simulate({ from: carl })).result).toBe(AMOUNT);
234+
},
235+
TEST_TIMEOUT,
236+
);
237+
238+
// --- transfer_public_to_commitment ---
239+
240+
it(
241+
'transfers public to a recipient commitment as a self-spend',
242+
async () => {
243+
const id = ID_A;
244+
// bob prepares the commitment for himself in his own settled tx, bound to alice (the payer) as completer.
245+
const commitment = await initializeMultiTokenTransferCommitment(multitoken, bob, bob, alice);
246+
await multitoken.methods.mint_to_public(alice, id, AMOUNT).send({ from: alice });
247+
const { receipt: transferTx } = await multitoken.methods
248+
.transfer_public_to_commitment(alice, id, commitment, AMOUNT, 0)
249+
.send({ from: alice });
250+
// transfer_public_to_commitment: TransferSingle(alice, PRIVATE, id, AMOUNT)
251+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, [
252+
{ from: alice, to: PRIVATE_ADDRESS, id, amount: AMOUNT },
253+
]);
254+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: alice })).result).toBe(0n);
255+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(AMOUNT);
256+
},
257+
TEST_TIMEOUT,
258+
);
259+
260+
// --- transfer_private_to_private ---
261+
262+
it(
263+
'transfers private to private as a self-spend and leaks nothing publicly',
264+
async () => {
265+
const id = ID_A;
266+
await multitoken.methods.mint_to_private(alice, id, AMOUNT).send({ from: alice });
267+
const { receipt: transferTx } = await multitoken.methods
268+
.transfer_private_to_private(alice, bob, id, AMOUNT, 0)
269+
.send({ from: alice });
270+
// transfer_private_to_private: (no public events)
271+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, []);
272+
expect((await multitoken.methods.balance_of_private(alice, id).simulate({ from: alice })).result).toBe(0n);
273+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(AMOUNT);
274+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: alice })).result).toBe(0n);
275+
expect((await multitoken.methods.balance_of_public(bob, id).simulate({ from: alice })).result).toBe(0n);
276+
// Third party never observes bob's new private notes.
277+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: carl })).result).toBe(0n);
278+
},
279+
TEST_TIMEOUT,
280+
);
281+
282+
// --- transfer_private_to_public ---
283+
284+
it(
285+
'transfers private to public as a self-spend and credits the recipient publicly',
286+
async () => {
287+
const id = ID_A;
288+
await multitoken.methods.mint_to_private(alice, id, AMOUNT).send({ from: alice });
289+
const { receipt: transferTx } = await multitoken.methods
290+
.transfer_private_to_public(alice, bob, id, AMOUNT, 0)
291+
.send({ from: alice });
292+
// transfer_private_to_public: TransferSingle(PRIVATE, bob, id, AMOUNT)
293+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, [
294+
{ from: PRIVATE_ADDRESS, to: bob, id, amount: AMOUNT },
295+
]);
296+
expect((await multitoken.methods.balance_of_private(alice, id).simulate({ from: alice })).result).toBe(0n);
297+
expect((await multitoken.methods.balance_of_public(bob, id).simulate({ from: alice })).result).toBe(AMOUNT);
298+
},
299+
TEST_TIMEOUT,
300+
);
301+
302+
// --- transfer_private_to_commitment ---
303+
304+
it(
305+
'transfers private to a settled commitment as a self-spend with no public event',
306+
async () => {
307+
const id = ID_A;
308+
// bob prepares the commitment for himself in a PRIOR settled tx, bound to alice (payer) as completer.
309+
// complete_from_private requires the validity commitment to be from a prior settled tx (the helper settles it).
310+
const commitment = await initializeMultiTokenTransferCommitment(multitoken, bob, bob, alice);
311+
await multitoken.methods.mint_to_private(alice, id, AMOUNT).send({ from: alice });
312+
const { receipt: transferTx } = await multitoken.methods
313+
.transfer_private_to_commitment(alice, id, commitment, AMOUNT, 0)
314+
.send({ from: alice });
315+
// transfer_private_to_commitment: (no public events) — private completion uses a private log, not a public write.
316+
await expectMultiTokenTransferEvents(transferTx.txHash, multitoken.address, []);
317+
expect((await multitoken.methods.balance_of_private(alice, id).simulate({ from: alice })).result).toBe(0n);
318+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(AMOUNT);
319+
},
320+
TEST_TIMEOUT,
321+
);
322+
323+
// --- burn_private ---
324+
325+
it(
326+
'burns from a private balance as a self-spend',
327+
async () => {
328+
const id = ID_A;
329+
await multitoken.methods.mint_to_private(alice, id, AMOUNT).send({ from: alice });
330+
await multitoken.methods.burn_private(alice, id, AMOUNT, 0).send({ from: alice });
331+
expect((await multitoken.methods.balance_of_private(alice, id).simulate({ from: alice })).result).toBe(0n);
332+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: alice })).result).toBe(0n);
333+
},
334+
TEST_TIMEOUT,
335+
);
336+
337+
// --- burn_public ---
338+
339+
it(
340+
'burns from a public balance and emits a burn event',
341+
async () => {
342+
const id = ID_A;
343+
await multitoken.methods.mint_to_public(alice, id, AMOUNT).send({ from: alice });
344+
const { receipt: burnTx } = await multitoken.methods.burn_public(alice, id, AMOUNT, 0).send({ from: alice });
345+
// burn_public: TransferSingle(alice, 0x0, id, AMOUNT)
346+
await expectMultiTokenTransferEvents(burnTx.txHash, multitoken.address, [
347+
{ from: alice, to: AztecAddress.ZERO, id, amount: AMOUNT },
348+
]);
349+
expect((await multitoken.methods.balance_of_public(alice, id).simulate({ from: alice })).result).toBe(0n);
350+
},
351+
TEST_TIMEOUT,
352+
);
353+
354+
// --- initialize_transfer_commitment ---
355+
356+
it(
357+
'initializes a transfer commitment (non-zero, no authwit, no balance change)',
358+
async () => {
359+
const id = ID_A;
360+
// Uses the sanctioned wallet-internals commitment helper (the ONLY sanctioned PXE/wallet-internals escape hatch).
361+
const commitment = await initializeMultiTokenTransferCommitment(multitoken, alice, bob, alice);
362+
expect(commitment).not.toBe(0n);
363+
// No id is bound at initialization; no balance is credited to the recipient or completer.
364+
expect((await multitoken.methods.balance_of_private(bob, id).simulate({ from: bob })).result).toBe(0n);
365+
expect((await multitoken.methods.balance_of_public(bob, id).simulate({ from: bob })).result).toBe(0n);
366+
},
367+
TEST_TIMEOUT,
368+
);
369+
});

0 commit comments

Comments
 (0)