Transferring Tokens with Aztec.js
In this guide, we will retrieving the Sandbox and deploy a pre-written contract to it using Aztec.js.
This guide assumes you have followed the quickstart.
Prerequisites
- A running Aztec sandbox
Set up the project
We will deploy a pre-compiled token contract, and send tokens privately, using the Sandbox.
We will create a yarn
TypeScript project called token
(although npm
works fine too).
- Initialize a yarn project
mkdir token
cd token
yarn init -yp
- Create a
src
folder inside your newtoken
directory:
mkdir src
- Add necessary yarn packages
yarn add @aztec/aztec.js @aztec/accounts @aztec/noir-contracts.js typescript @types/node
- Add a
tsconfig.json
file into the project root and paste this:
{
"compilerOptions": {
"outDir": "dest",
"rootDir": "src",
"target": "es2020",
"lib": ["dom", "esnext", "es2017.object"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"downlevelIteration": true,
"inlineSourceMap": true,
"declarationMap": true,
"importHelpers": true,
"resolveJsonModule": true,
"composite": true,
"skipLibCheck": true
},
"references": [],
"include": ["src", "src/*.json"]
}
- Add this to your
package.json
:
"type": "module",
"scripts": {
"build": "yarn clean && tsc -b",
"build:dev": "tsc -b --watch",
"clean": "rm -rf ./dest tsconfig.tsbuildinfo",
"start": "yarn build && LOG_LEVEL='info: token' node ./dest/index.js"
},
- Create an
index.ts
file in thesrc
directory with the following sandbox connection setup:
import { getSchnorrAccount } from '@aztec/accounts/schnorr';
import { getDeployedTestAccountsWallets } from '@aztec/accounts/testing';
import { Fr, GrumpkinScalar, type PXE, createLogger, createPXEClient, waitForPXE } from '@aztec/aztec.js';
import { format } from 'util';
import { type AztecAddress, type Logger, type Wallet } from '@aztec/aztec.js';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
export async function deployToken(adminWallet: Wallet, initialAdminBalance: bigint, logger: Logger) {
logger.info(`Deploying Token contract...`);
const contract = await TokenContract.deploy(adminWallet, adminWallet.getAddress(), 'TokenName', 'TokenSymbol', 18)
.send()
.deployed();
if (initialAdminBalance > 0n) {
// Minter is minting to herself so contract as minter is the same as contract as recipient
await mintTokensToPrivate(contract, adminWallet, adminWallet.getAddress(), initialAdminBalance);
}
logger.info('L2 contract deployed');
return contract;
}
export async function mintTokensToPrivate(
token: TokenContract,
minterWallet: Wallet,
recipient: AztecAddress,
amount: bigint,
) {
const tokenAsMinter = await TokenContract.at(token.address, minterWallet);
const from = minterWallet.getAddress(); // we are setting from to minter here because of TODO(#9887)
await tokenAsMinter.methods.mint_to_private(from, recipient, amount).send().wait();
}
const { PXE_URL = 'http://localhost:8080' } = process.env;
async function main() {
////////////// CREATE THE CLIENT INTERFACE AND CONTACT THE SANDBOX //////////////
const logger = createLogger('e2e:token');
// We create PXE client connected to the sandbox URL
const pxe = createPXEClient(PXE_URL);
// Wait for sandbox to be ready
await waitForPXE(pxe, logger);
const nodeInfo = await pxe.getNodeInfo();
logger.info(format('Aztec Sandbox Info ', nodeInfo));
}
main();
- Finally, run the package:
In the project root, run
yarn start
A successful run should show something like this:
token Aztec Sandbox Info {
token sandboxVersion: '0.68.0',
token chainId: 31337,
token protocolVersion: 1,
token l1ContractAddresses: {
token rollupAddress: EthAddress {
token buffer: <Buffer cf 7e d3 ac ca 5a 46 7e 9e 70 4c 70 3e 8d 87 f6 34 fb 0f c9>
token },
token registryAddress: EthAddress {
token buffer: <Buffer 5f bd b2 31 56 78 af ec b3 67 f0 32 d9 3f 64 2f 64 18 0a a3>
token },
token inboxAddress: EthAddress {
token buffer: <Buffer e7 f1 72 5e 77 34 ce 28 8f 83 67 e1 bb 14 3e 90 bb 3f 05 12>
token },
token outboxAddress: EthAddress {
token buffer: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
token },
token }
token } +0ms
Great! The Sandbox is running and we are able to interact with it.
Load accounts
The sandbox is preloaded with multiple accounts so you don't have to sit and create them. Let's load these accounts. Add this code to the main()
function in index.ts
below the code that's there:
////////////// LOAD SOME ACCOUNTS FROM THE SANDBOX //////////////
// The sandbox comes with a set of created accounts. Load them
const accounts = await getDeployedTestAccountsWallets(pxe);
const aliceWallet = accounts[0];
const bobWallet = accounts[1];
const alice = aliceWallet.getAddress();
const bob = bobWallet.getAddress();
logger.info(`Loaded alice's account at ${alice.toString()}`);
logger.info(`Loaded bob's account at ${bob.toString()}`);
Source code: yarn-project/end-to-end/src/composed/e2e_sandbox_example.test.ts#L89-L99
An explanation on accounts on Aztec can be found here.
Deploy a contract
Now that we have our accounts loaded, let's move on to deploy our pre-compiled token smart contract. You can find the full code for the contract here (GitHub link). Add this to index.ts
below the code you added earlier:
////////////// DEPLOY OUR TOKEN CONTRACT //////////////
const initialSupply = 1_000_000n;
const tokenContractAlice = await deployToken(aliceWallet, initialSupply, logger);
Source code: yarn-project/end-to-end/src/composed/e2e_sandbox_example.test.ts#L101-L107
yarn start
will now give something like this:
token Aztec Sandbox Info {
token sandboxVersion: '0.68.0',
token chainId: 31337,
token protocolVersion: 1,
token l1ContractAddresses: {
token rollupAddress: EthAddress {
token buffer: <Buffer cf 7e d3 ac ca 5a 46 7e 9e 70 4c 70 3e 8d 87 f6 34 fb 0f c9>
token },
token registryAddress: EthAddress {
token buffer: <Buffer 5f bd b2 31 56 78 af ec b3 67 f0 32 d9 3f 64 2f 64 18 0a a3>
token },
token inboxAddress: EthAddress {
token buffer: <Buffer e7 f1 72 5e 77 34 ce 28 8f 83 67 e1 bb 14 3e 90 bb 3f 05 12>
token },
token outboxAddress: EthAddress {
token buffer: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
token },
token }
token } +0ms
token Loaded alice's account at 0x25048e8c...70d0 +4s
token Loaded bob's account at 0x115f123b...6483 +0ms
token Deploying token contract... +0ms
token Contract successfully deployed at address 0x11a03dce...afc7 +5s
token Minting tokens to Alice... +18ms
token 1000000 tokens were successfully minted and redeemed by Alice +10s
We can break this down as follows:
- We create and send a contract deployment transaction to the network.
- We wait for it to be successfully mined.
- We retrieve the transaction receipt containing the transaction status and contract address.
- We connect to the contract with Alice
- Alice initialize the contract with herself as the admin and a minter.
- Alice privately mints 1,000,000 tokens to herself
View the balance of an account
A token contract wouldn't be very useful if you aren't able to query the balance of an account. As part of the deployment, tokens were minted to Alice. We can now call the contract's balance_of_private()
function to retrieve the balances of the accounts.
Call the balance_of_private
function using the following code (paste this):
////////////// QUERYING THE TOKEN BALANCE FOR EACH ACCOUNT //////////////
// Bob wants to mint some funds, the contract is already deployed, create an abstraction and link it his wallet
// Since we already have a token link, we can simply create a new instance of the contract linked to Bob's wallet
const tokenContractBob = tokenContractAlice.withWallet(bobWallet);
let aliceBalance = await tokenContractAlice.methods.balance_of_private(alice).simulate();
logger.info(`Alice's balance ${aliceBalance}`);
let bobBalance = await tokenContractBob.methods.balance_of_private(bob).simulate();
logger.info(`Bob's balance ${bobBalance}`);
Source code: yarn-project/end-to-end/src/composed/e2e_sandbox_example.test.ts#L112-L126
Running now should yield output:
token Aztec Sandbox Info {
token sandboxVersion: '0.68.0',
token chainId: 31337,
token protocolVersion: 1,
token l1ContractAddresses: {
token rollupAddress: EthAddress {
token buffer: <Buffer cf 7e d3 ac ca 5a 46 7e 9e 70 4c 70 3e 8d 87 f6 34 fb 0f c9>
token },
token registryAddress: EthAddress {
token buffer: <Buffer 5f bd b2 31 56 78 af ec b3 67 f0 32 d9 3f 64 2f 64 18 0a a3>
token },
token inboxAddress: EthAddress {
token buffer: <Buffer e7 f1 72 5e 77 34 ce 28 8f 83 67 e1 bb 14 3e 90 bb 3f 05 12>
token },
token outboxAddress: EthAddress {
token buffer: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
token },
token }
token } +0ms
token Loaded alice's account at 0x25048e8c...70d0 +4s
token Loaded bob's account at 0x115f123b...6483 +0ms
token Deploying token contract... +0ms
token Contract successfully deployed at address 0x1b388d99...4b55 +4s
token Minting tokens to Alice... +10ms
token 1000000 tokens were successfully minted and redeemed by Alice +10s
token Alice's balance 1000000 +80ms
token Bob's balance 0 +31ms
Above, we created a second instance of the TokenContract
contract class.
This time pertaining to Bob.
This class offers a TypeScript bindings of our Token
contract..
We then call balance_of_private()
as a view
method.
View methods can be thought as read-only.
No transaction is submitted as a result but a user's state can be queried.
We can see that each account has the expected balance of tokens.
Calling an unconstrained (view) function
Create and submit a transaction
Transfer
Now lets transfer some funds from Alice to Bob by calling the transfer
function on the contract. This function takes 2 arguments:
- The recipient.
- The quantity of tokens to be transferred.
Here is the Typescript code to call the transfer
function, add this to your index.ts
at the bottom of the main
function:
////////////// TRANSFER FUNDS FROM ALICE TO BOB //////////////
// We will now transfer tokens from ALice to Bob
const transferQuantity = 543n;
logger.info(`Transferring ${transferQuantity} tokens from Alice to Bob...`);
await tokenContractAlice.methods.transfer(bob, transferQuantity).send().wait();
// Check the new balances
aliceBalance = await tokenContractAlice.methods.balance_of_private(alice).simulate();
logger.info(`Alice's balance ${aliceBalance}`);
bobBalance = await tokenContractBob.methods.balance_of_private(bob).simulate();
logger.info(`Bob's balance ${bobBalance}`);
Source code: yarn-project/end-to-end/src/composed/e2e_sandbox_example.test.ts#L131-L145
Our output should now look like this:
token Aztec Sandbox Info {
token sandboxVersion: '0.68.0',
token chainId: 31337,
token protocolVersion: 1,
token l1ContractAddresses: {
token rollupAddress: EthAddress {
token buffer: <Buffer cf 7e d3 ac ca 5a 46 7e 9e 70 4c 70 3e 8d 87 f6 34 fb 0f c9>
token },
token registryAddress: EthAddress {
token buffer: <Buffer 5f bd b2 31 56 78 af ec b3 67 f0 32 d9 3f 64 2f 64 18 0a a3>
token },
token inboxAddress: EthAddress {
token buffer: <Buffer e7 f1 72 5e 77 34 ce 28 8f 83 67 e1 bb 14 3e 90 bb 3f 05 12>
token },
token outboxAddress: EthAddress {
token buffer: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
token },
token }
token } +0ms
token Loaded alice's account at 0x25048e8c...70d0 +4s
token Loaded bob's account at 0x115f123b...6483 +0ms
token Deploying token contract... +0ms
token Contract successfully deployed at address 0x01d8af7d...9a4d +5s
token Minting tokens to Alice... +18ms
token 1000000 tokens were successfully minted and redeemed by Alice +11s
token Alice's balance 1000000 +59ms
token Bob's balance 0 +33ms
token Transferring 543 tokens from Alice to Bob... +0ms
token Alice's balance 999457 +6s
token Bob's balance 543 +39ms
Here, we used the same contract abstraction as was previously used for reading Alice's balance. But this time we called send()
generating and sending a transaction to the network. After waiting for the transaction to settle we were able to check the new balance values.
Mint
Finally, the contract has several mint
functions that can be used to generate new tokens for an account.
We will focus only on mint_to_private
.
This function has private and public execution components, but it mints tokens privately.
This function takes:
- A minter (
from
) - A recipient
- An amount of tokens to mint
This function starts as private to set up the creation of a partial note. The private function calls a public function that checks that the minter is authorized to mint new tokens an increments the public total supply. The recipient of the tokens remains private, but the minter and the amount of tokens minted are public.
Let's now use these functions to mint some tokens to Bob's account using Typescript, add this to index.ts
:
////////////// MINT SOME MORE TOKENS TO BOB'S ACCOUNT //////////////
// Now mint some further funds for Bob
// Alice is nice and she adds Bob as a minter
await tokenContractAlice.methods.set_minter(bob, true).send().wait();
const mintQuantity = 10_000n;
await mintTokensToPrivate(tokenContractBob, bobWallet, bob, mintQuantity);
// Check the new balances
aliceBalance = await tokenContractAlice.methods.balance_of_private(alice).simulate();
logger.info(`Alice's balance ${aliceBalance}`);
bobBalance = await tokenContractBob.methods.balance_of_private(bob).simulate();
logger.info(`Bob's balance ${bobBalance}`);
Source code: yarn-project/end-to-end/src/composed/e2e_sandbox_example.test.ts#L150-L167
Our complete output should now be something like:
token Aztec Sandbox Info {
token sandboxVersion: '0.68.0',
token chainId: 31337,
token protocolVersion: 1,
token l1ContractAddresses: {
token rollupAddress: EthAddress {
token buffer: <Buffer cf 7e d3 ac ca 5a 46 7e 9e 70 4c 70 3e 8d 87 f6 34 fb 0f c9>
token },
token registryAddress: EthAddress {
token buffer: <Buffer 5f bd b2 31 56 78 af ec b3 67 f0 32 d9 3f 64 2f 64 18 0a a3>
token },
token inboxAddress: EthAddress {
token buffer: <Buffer e7 f1 72 5e 77 34 ce 28 8f 83 67 e1 bb 14 3e 90 bb 3f 05 12>
token },
token outboxAddress: EthAddress {
token buffer: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
token },
token }
token } +0ms
token Loaded alice's account at 0x25048e8c...70d0 +4s
token Loaded bob's account at 0x115f123b...6483 +0ms
token Deploying token contract... +0ms
token Contract successfully deployed at address 0x03a0bb2c...02c2 +7s
token Minting tokens to Alice... +19ms
token 1000000 tokens were successfully minted and redeemed by Alice +9s
token Alice's balance 1000000 +43ms
token Bob's balance 0 +31ms
token Transferring 543 tokens from Alice to Bob... +0ms
token Alice's balance 999457 +6s
token Bob's balance 543 +36ms
token Minting 10000 tokens to Bob... +5s
token Alice's balance 999457 +9s
token Bob's balance 10543 +43ms
That's it! We have successfully deployed a token contract to an instance of the Aztec network and mined private state-transitioning transactions. We have also queried the resulting state all via the interfaces provided by the contract. To see exactly what has happened here, you can learn about the transaction flow on the Concepts page here.
Next Steps
Build a fullstack Aztec project
Follow the dapp tutorial.