Skip to main content
Version: dev

Deploying a Token Contract

In this guide, we will retrieving the Sandbox and deploy a pre-written token contract to it using Aztec.js. Check out the source code. We will then use Aztec.js to interact with this contract and transfer tokens.

Before starting, make sure to be running Aztec sandbox at version latest. Check out the guide for info about that.

Set up the project

Let's initialize a yarn project first. You can init it with the dependencies we need:

yarn add typescript @types/node tsx
tip

Never heard of tsx? Well, it will just run typescript with reasonable defaults. Pretty cool for a small example like this one. You may want to tune in your own project's tsconfig.json later!

Let's also import the Aztec dependencies for this tutorial:

yarn add @aztec/aztec.js@latest @aztec/accounts@latest @aztec/noir-contracts.js@latest

Aztec.js assumes your project is using ESM, so make sure you add "type": "module" to package.json. You probably also want at least a start script. For example:

{
"type": "module",
"scripts": {
"start": "tsx index.ts"
}
}

Connecting to the sandbox

We want to connect to our running sandbox and import the test accounts into the local PXE. Let's call them Alice and Bob (of course). Create an index.ts with it:

import { createPXEClient } from '@aztec/aztec.js';
import { getDeployedTestAccountsWallets } from '@aztec/accounts/testing'
const pxe = await createPXEClient('http://localhost:8080');

const alice = (await getDeployedTestAccountsWallets(pxe))[0];
const bob = (await getDeployedTestAccountsWallets(pxe))[1];

Deploy the token 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).

Let's import the interface directly, and make Alice the admin:

import { TokenContract } from '@aztec/noir-contracts.js/Token';

const token = await TokenContract.deploy(alice, alice.getAddress(), 'TokenName', 'TKN', 18)
.send({ from: alice.getAddress() })
.deployed();

Mint and transfer

Let's go ahead and have Alice mint herself some tokens, in private:

await token.methods.mint_to_private(alice.getAddress(), 100).send({ from: alice.getAddress() }).wait();

Let's check both Alice's and Bob's balances now:

let aliceBalance = await token.methods.balance_of_private(alice.getAddress()).simulate({ from: alice.getAddress() });
console.log(`Alice's balance: ${aliceBalance}`); // whoooaa 100 tokens
let bobBalance = await token.methods.balance_of_private(bob.getAddress()).simulate({ from: bob.getAddress() });
console.log(`Bob's balance: ${bobBalance}`) // you get nothin' 🥹

Great! Let's have Alice transfer some tokens to Bob, also in private:

await token.methods.transfer(bob.getAddress(), 10).send({ from: alice.getAddress() }).wait();
bobBalance = await token.methods.balance_of_private(bob.getAddress()).simulate({ from: bob.getAddress() });
console.log(`Bob's balance: ${bobBalance}`)

Nice, Bob should now see 10 tokens in his balance! Thanks Alice!

Other cool things

Say that Alice is nice and wants to set Bob as a minter. Even though it's a public function, it can be called in a similar way:

await token.methods.set_minter(bob.getAddress(), true).send({ from: alice.getAddress() }).wait();

Bob is now the minter, so he can mint some tokens to himself, notice that for the time being, you need to bind token to Bob's wallet with withWallet(bob):

await token.withWallet(bob).methods.mint_to_private(bob.getAddress(), 100).send({ from: bob.getAddress() }).wait();
bobBalance = await token.methods.balance_of_private(bob.getAddress()).simulate({ from: bob.getAddress() });
console.log(`Bob's balance: ${bobBalance}`)
info

Have a look at the contract source. Notice is that the mint_to_private function we used above actually starts a partial note. This allows the total balance to increase while keeping the recipient private! How cool is that?