Default Account State
Create token that requires interaction with a specific service to use.
Summary
- The
default state
extension enables developers to set new token accounts for a mint with this extension to be frozen by default, requiring interaction with a specific service to unfreeze and utilize the tokens. - There are three states of token accounts: Initialized, Uninitialized, and Frozen, which determine how a token account can be interacted with.
- When a token account is frozen, the balance cannot change.
- The
freezeAuthority
is the only address that can freeze and thaw a token account - The
default state
can be updated usingupdateDefaultAccountState
- The lab demonstrates creating a mint with the
default state
extension and creating a new token account which is set to a frozen state upon creation. The lab includes tests to ensure the extension works as intended for both minting and transferring tokens in frozen and thawed states.
Overview
The default state
extension allows developers to force all new token accounts
to be in one of two states: “Initialized” or “Frozen”. Most usefully, with this
extension any new token accounts created can be set to frozen. When a token
account is frozen, it’s balance cannot change. Meaning it cannot be minted to,
transferred from or burned. Only the freezeAuthority
can thaw a frozen
account.
Imagine you’re a Nexis Native Chain game dev, and you only want players of your game to
interact with your in-game token. You can make the player, sign up for the game
to thaw their token account and allow them to play and trade with other players.
This works because of the default state
extension, where it is set that all
new token accounts are frozen.
Types of States
There are 3 types of state with the default account state extension:
- Uninitialized: This state indicates that the token account has been created but not yet initialized through the Token Program.
- Initialized: An account in the Initialized state has been properly set up through the Token Program. This means it has a specified mint and an owner has been assigned.
- Frozen: A Frozen account is one that has been temporarily disabled from performing certain operations, specifically transferring and minting tokens.
However, default state
only deals with the latter two: Initialized
and
Frozen
. When you freeze an account, the state is Frozen
, when you thaw, it
is Initialized
.
Adding default account state
Initializing a mint with transfer fee involves three instructions:
SystemProgram.createAccount
createInitializeTransferFeeConfigInstruction
createInitializeMintInstruction
The first instruction SystemProgram.createAccount
allocates space on the
blockchain for the mint account. This instruction accomplishes three things:
- Allocates
space
- Transfers
lamports
for rent - Assigns to it’s owning program
To grab the size of the mint account, we call getMintLen
, and to grab the
lamports needed for the space, we call getMinimumBalanceForRentExemption
.
The second
instruction createInitializeDefaultAccountStateInstruction
initializes the
default account state extension.
The third instruction createInitializeMintInstruction
initializes the mint.
Lastly, add all of these instructions to a transaction and send it to the blockchain.
Updating the Default Account State
You can always change the default account state assuming you have the authority
to do so. To do this, simply call updateDefaultAccountState
.
Updating the Freeze Authority
Lastly, you may want to update the freezeAuthority
to another account. Say you
want to handle the freezing and thawing by a program for example. You can do
this, by calling setAuthority
, adding in the correct accounts and passing in
the authorityType
, which in this case would be AuthorityType.FreezeAccount
.
Lab
In this lab we will be creating a mint which all new token accounts are frozen
upon creation by using the default state
extension. We will then write tests
to check if the extension is working as intended by attempting to mint and
transfer the tokens in a frozen and thawed account state.
1. Setup Environment
To get started, create an empty directory named default-account-state
and
navigate to it. We’ll be initializing a brand new project. Run npm init
and
follow through the prompts.
Next, we’ll need to add our dependencies. Run the following to install the required packages:
Create a directory named src
. In this directory, create a file named
index.ts
. This is where we will run checks against the rules of this
extension. Paste the following code in index.ts
:
index.ts
creates a connection to the specified validator node and calls
initializeKeypair
. It also has a few variables we will be using in the rest of
this lab. The index.ts
is where we’ll end up calling the rest of our script
once we’ve written it.
If you run into an error in initializeKeypair
with airdropping, follow the
next step.
2. Run validator node
For the sake of this guide, we’ll be running our own validator node.
In a separate terminal, run the following command: solana-test-validator
. This
will run the node and also log out some keys and values. The value we need to
retrieve and use in our connection is the JSON RPC URL, which in this case is
http://127.0.0.1:8899
. We then use that in the connection to specify to use
the local RPC URL.
Alternatively, if you’d like to use testnet or devnet, import the
clusterApiUrl
from @nexis-network/web3.js
and pass it to the connection as such:
If you decide to use devnet, and have issues with airdropping sol. Feel free to
add the keypairPath
parameter to initializeKeypair
. You can get this from
running solana config get
in your terminal. And then go to
faucet.nexis.network and airdrop some sol to your
address. You can get your address from running solana address
in your
terminal.
3. Helpers
When we pasted the index.ts
code from earlier, we added the following helpers:
initializeKeypair
: This function creates the keypair for thepayer
and also airdrops some NZT to itmakeKeypairs
: This function creates keypairs without airdropping any NZT
Additionally we have some initial accounts:
payer
: Used to pay for and be the authority for everythingmintKeypair
: Our mint that will have thedefault state
extensionourTokenAccountKeypair
: The token account owned by payer that we’ll use for testingotherTokenAccountKeypair
: Another token used for testing
4. Create Mint with Default account state
When creating a mint token with default state, we must create the account instruction, initialize the default account state for the mint account and initialize the mint itself.
Create an asynchronous function named createTokenExtensionMintWithDefaultState
in src/mint-helpers.ts
. This function will create the mint such that all new
token accounts will be “frozen” to start. The function will take the following
arguments:
connection
: The connection objectpayer
: Payer for the transactionmintKeypair
: Keypair for the new mintdecimals
: Mint decimalsdefaultState
: Mint token default state - eg:AccountState.Frozen
The first step in creating a mint is reserving space on Nexis Native Chain with the
SystemProgram.createAccount
method. This requires specifying the payer’s
keypair, (the account that will fund the creation and provide NZT for rent
exemption), the new mint account’s public key (mintKeypair.publicKey
), the
space required to store the mint information on the blockchain, the amount of
NZT (lamports) necessary to exempt the account from rent and the ID of the token
program that will manage this mint account (TOKEN_2022_PROGRAM_ID
).
After the mint account creation, the next step involves initializing it with the
default state. The createInitializeDefaultAccountStateInstruction
function is
used to generate an instruction that enables the mint to set defaultState
of
any new token accounts.
Next, lets add the mint instruction by calling createInitializeMintInstruction
and passing in the required arguments. This function is provided by the SPL
Token package and it constructs a transaction instruction that initializes a new
mint.
Lastly, let’s add all of the instructions to a transaction and send it to the blockchain:
Putting it all together, the final src/mint-helpers.ts
file will look like
this:
6. Test Setup
Now that we have the ability to create a mint with a default state for all of it’s new token accounts, let’s write some tests to see how it functions.
6.1 Create Mint with Default State
Let’s first create a mint with the default state of frozen
. To do this we call
the createTokenExtensionMintWithDefaultState
function we just created in out
index.ts
file:
6.2 Create Test Token Accounts
Now, let’s create two new Token accounts to test with. We can accomplish this by
calling the createAccount
helper provided by the SPL Token library. We will
use the keypairs we generated at the beginning: ourTokenAccountKeypair
and
otherTokenAccountKeypair
.
7 Tests
Now let’s write some tests to show the interactions that can be had with the
default state
extension.
We’ll write four tests in total:
- Minting without thawing the recipient’s account
- Minting with thawing the recipient’s account
- Transferring without thawing the recipient’s account
- Transferring with thawing the recipient’s account
7.1 Minting without thawing the recipient’s account
This test will attempt to mint a token to ourTokenAccount
without thawing the
account. This test is expected to fail as the account will be frozen on the mint
attempt. Remember: when a token account is frozen, the balance cannot change.
To do this, let’s wrap a mintTo
function in a try catch
and print out the
respected result:
Test this by running the script:
We should see the following error logged out in the terminal, meaning the
extension is working as intended.
✅ - We expected this to fail because the account is still frozen.
7.2 Minting with thawing the recipient’s account
This test will attempt to mint a token after thawing the token account. This test is expected to pass as the account will be thawed on the mint attempt.
We can create this test by calling thawAccount
and then mintTo
:
Go ahead and run the script, the transaction should succeed.
7.3 Transferring without thawing the recipient’s account
Now that we’ve tested minting, we can test transferring our tokens frozen and
not. First lets test a transfer without thawing the recipient’s token account.
Remember, by default, the otherTokenAccountKeypair
is frozen due to the
extension.
Again, we expect this test to fail, since the otherTokenAccountKeypair
is
frozen and it’s balance cannot change.
To test this, let’s wrap a transfer
function in a try catch
:
Run the test and see the results:
7.4 Transferring with thawing the recipient’s account
The last test we’ll create tests transferring tokens after thawing the token account we will be transferring to. This test is expected to pass, since all token accounts will now be thawed.
We’ll do this by calling thawAccount
and then transfer
:
Run all of the tests one last time and see the results:
Remember the key takeaways:
- The
default state
extension, enforces the default state on all new token accounts. - Frozen account’s balance cannot change.
Congratulations! We’ve just created and tested a mint using the default account extension!
Challenge
Add tests for burning tokens from frozen and thawed token accounts (hint, one will fail, one will succeed).
To get you started: