Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.
Custom Token API
This section shows you how to perform common token operations, such as minting, burning, and sending tokens, using SnarkyJS.
Minting
Minting generates new tokens whereby the zkApp updates the balance of an account by adding the newly created tokens to it. Minted tokens can be sent to any existing account in the ledger.
To mint new tokens using a zkApp, we access the this.token
property on our SmartContract
class. See the example below of how a zkApp can mint tokens to another account:
class MintExample extends SmartContract {
...
@method mintNewTokens(receiverAddress: PublicKey) {
this.token.mint({
address: receiverAddress,
amount: 100_000,
});
}
}
In the snippet above, we define a smart contract called MintExample
with a method called mintNewTokens
. Using this.token
, the smart contract specifies the address to mint new tokens for as well as the amount.
Burning
Burning tokens is the opposite of minting. Burning tokens deducts the balance of a certain address by the specified amount. See the example below of how a zkApp can burn tokens of another account:
class BurnExample extends SmartContract {
...
@method burnTokens(addressToDecrease: PublicKey) {
this.token.burn({
address: addressToDecrease,
amount: 100_000,
});
}
}
In the snippet above, we define a smart contract called BurnExample
with a method called burnTokens
. Similar to minting, we use the this.token
property to call the burn
method. This specifies the amount of tokens to burn for the specified address.
A zkApp cannot burn more tokens than the specified account has. If there is such a case, an error is thrown and no such transaction is made.
Sending
To send a custom token, use the send()
method available on this.token
. See the example below of how a zkApp can approve sending tokens between two accounts:
class SendExample extends SmartContract {
...
@method sendTokens(
senderAddress: PublicKey,
receiverAddress: PublicKey,
amount: UInt64
) {
this.token.send({
to: receiverAddress,
from: senderAddress,
amount,
});
}
}
In the snippet above, we define a smart contract called SendExample
with a method called sendTokens
. Then, in the same fashion, as minting and burning, we use the this.token
property to call the send
method.
For a more comprehensive example of how to use custom tokens with a zkApp, please see this custom token example
Authorization
When a zkApp interacts with a custom token that it did not originally create, the calling zkApp must get authorization from the token owner.
There are two ways for a token owner to approve a transaction, one being a signature and the other being a proof.
Signature Authorization
Signature authorization is the simplest way for a token owner to approve a custom token transfer; however, it is the least flexible. If two separate accounts want to trade a specific custom token, the token owner must sign the transaction before the transaction can be broadcast.
Token zkApps should provide a send()
method, like the one described above. By providing a send()
method, the token zkApp can approve a transaction between two seperate accounts and sign it as the token owner.
let tx = await Mina.transaction(feePayerKey, () => {
zkapp.sendTokens(senderAddress, receiverAddress, UInt64.from(1_000)); // Sends 1,000 tokens from `senderAddress` to `receiverAddress`
zkapp.sign(zkappKey); // Signs the transaction with the token owner key
});
await tx.send();
This sort of authorization is simple and easy to use but it is not flexible. If two accounts wanted to trade a custom token, the token owner must sign the transaction before the transaction can be broadcasted. To allow zkApps to get authorization from a token owner without a signature, we can let the token owner approve with a proof.
Proof Authorization
Proof authorization is a more flexible way for a token owner to approve a custom token transfer. If two separate accounts want to trade a specific custom token, the token owner can provide a proof that the transaction is valid. This allows the token owner to approve a transaction without signing it.
To allow for proof authorization by the token owner, the child zkApp that is requesting authorization must provide a way for the token owner to inspect the changes it wants to make and verify that they are valid. Token owner contracts have the power to inspect child account updates to enforce custom token rules. For example, a token owner contract could enforce that a child zkApp can only send tokens to a specific address.
Token owner contracts can inspect the updates that a child zkApp wants to make by using a combination of Experimental.Callback
and this.approve
. The first thing that a token contract must do is generate the account updates that a child zkApp wants to make. The child zkApp wraps a function around Experimental.Callback
which contains the changes it wants to make. The token owner can then execute that function with this.approve
and inspect the changes that the child zkApp wants to make.
See the example below of how a zkApp can approve a transaction between two accounts. In the first example, we will show how to approve a transaction by calling a specified SmartContract
method.
/**
* This TokenContract class is used to create a custom token
* and acts as the token owner of the custom token
*/
class TokenContract extends SmartContract {
...
/**
* 'sendTokens()' sends tokens from `senderAddress` to `receiverAddress`.
*
* It does so by deducting the amount of tokens from `senderAddress` by
* authorizing the deduction with a proof. It then creates the receiver
* from `receiverAddress` and sends the amount.
*/
@method sendTokens(
senderAddress: PublicKey,
receiverAddress: PublicKey,
amount: UInt64,
callback: Experimental.Callback<any>
) {
// approves the callback which deductes the amount of tokens from the sender
let senderAccountUpdate = this.approve(callback);
// Create constraints for the sender account update and amount
let negativeAmount = Int64.fromObject(
senderAccountUpdate.body.balanceChange
);
negativeAmount.assertEquals(Int64.from(amount).neg());
let tokenId = this.token.id;
// Create receiver accountUpdate
let receiverAccountUpdate = Experimental.createChildAccountUpdate(
this.self,
receiverAddress,
tokenId
);
receiverAccountUpdate.balance.addInPlace(amount);
}
}
class ZkAppB extends SmartContract {
/*
* This method is used to get authorization from the token owner. Remember,
* the token owner is the one who created the custom token. To debit their
* balance, we must get authorization from the token owner
*/
@method approveSend(amount: UInt64) {
this.balance.subInPlace(amount);
}
}
let tx = await Local.transaction(feePayer, () => {
let amount = UInt64.from(1_000)
// Create a callback inside the transaction that calls the approveSend method.
// This will be executed by the token owner to get authorization.
let approveSendingCallback = Experimental.Callback.create(
zkAppB,
'approveSend',
[amount]
);
// Here, we call the token contract with the callback
tokenZkApp.sendTokens(zkAppBAddress, account1Address, amount, approveSendingCallback);
});
await tx.prove();
tx.sign([zkAppBKey]);
await tx.send();
The result of the example above is zkAppB
sending tokens to account1Address
and account1Address
receiving tokens from zkAppB
. The transaction is approved by the token owner without the token owner having to sign the transaction.
For more examples of how to approve a transaction with a zkApp, please see this authorization example.
Understanding Important Terms
If you are writing your zkApp to interact with custom tokens, you should understand the following essential terms:
Token Id
Token Ids are unique identifiers that are used to distinguish between different types of custom tokens. Custom token identifiers are globally unique across the entire network, meaning no two custom tokens can have the same token id.
Token Ids themeselves are derived from a zkApp. To check the token id of a zkApp, you can use the this.token.id
property.
Token Accounts
Token accounts are like regular accounts, but they hold a balance of a specific custom token instead of MINA. A token account is created from an existing account but is instead specified by a public key and token id. If an existing account receives a transaction that is specified by a custom token, a token account for that public key and token id will be created if it does not exist.
Token accounts are specific for each type of custom token, meaning that a single public key can have many different types of token accounts.
To create a token account for a specified custom token, a token account is automatically created for that public key whenever an existing account receives a transaction denoted with a custom token." to "A token account is automatically created for a public key whenever an existing account receives a transaction denoted with a custom token.
Suppose a token account is being created for the first time. In that case, an account creation fee must be paid similarly to creating a new standard account.
Aside from sending custom tokens, custom tokens can be minted and burned by a token owner account. A token owner account is the governing zkApp account for a specific custom token.
Token Owner
A token owner is an account that creates, facilitates, and governs how a custom token is to be used. Concretely, the token owner is the account that created the custom token and is the only account that can mint and burn tokens.
In addition to being the only account that can mint and burn tokens, the token owner is also the only account that can approve sending tokens between two accounts, by either using signature authorization or proof authorization. If two accounts want to send tokens to each other, the token owner must approve the transaction. The token owner generates the changes the two accounts want to make and can then make assertions about those changes. The token owner can approve the transaction either with a signature or proof.