Master Aptos Assets: A Guide to Standards, Allowlists, and Token Gating

Master Aptos Assets: A Guide to Standards, Allowlists, and Token Gating

Aptos is built on a design that focuses on being modular, efficient, and scalable. We'll look at the main parts that make Aptos a strong and flexible platform for decentralized apps. It has a special account structure and advanced systems for managing objects and assets. This smart design makes it both flexible and powerful. We will explore its unique structure, from accounts to objects, in the first half.

In the second half, we'll walk you through creating a sophisticated token-minting dapp (Github Repo) with advanced features like allowlisting and token-gated access. You'll gain hands-on experience in:

  • Setting up a development environment for Aptos

  • Creating and deploying smart contracts using the Move language

  • Implementing a fungible asset (FA) token system

  • Building a user-friendly frontend interface

  • Adding an allowlist for controlled minting access

  • Creating token-gated content for exclusive user experiences

Ready to dive in? Let's start by looking at this important structural element.

Fundamental Structure

Accounts, Data, and Logic

Accounts

Accounts can hold resources and publish modules.

  • User Accounts: Standard accounts controlled by users with private keys.

  • Resource Accounts: Autonomous accounts without private keys, used by developers for special purposes. A resource account allows the module to sign for other modules and transactions.

Data

Data can be stored in both user accounts and resource accounts. An object can own multiple other objects, creating parent-child relationships. This allows for the creation of complex data structures like collections, where a collection object can own multiple token objects.

  • Resources: Move language primitives that represent assets or data, stored within accounts.

  • Objects: Complex sets of resources stored at a single address, representing a single entity.

Logic

Both user accounts and resource accounts can publish modules.

  • Modules: Smart contracts written in Move that can be published to accounts.

User Interaction

The diagram shows how users interact with accounts and resources in a system:

  1. Users create accounts or resource accounts.

  2. Accounts can own and manage resources or objects.

  3. Resource accounts are mainly for publishing modules and managing autonomous contracts.

  4. Once published, modules can also manage resources or objects.

  5. Objects can be owned by other objects, allowing nested structures.

This flexible setup enables advanced on-chain interactions, useful for applications like DeFi, gaming, and digital asset management.

Optimization: Resource Groups

Resource groups in Aptos let you store multiple Move resources together in one storage slot. This has several benefits:

  1. Improved storage efficiency: Grouping related resources reduces the overhead of storing each one separately.

  2. Enhanced upgradeability: Resource groups make it easier to update data structures over time.

  3. Better performance: Reading and writing grouped resources is more efficient than accessing many separate ones.

Resource groups are useful for:

  • Creating complex data structures like NFTs or marketplaces

  • Updating data models without breaking existing structures

  • Optimizing storage and access for frequently used resources

#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct Listing has key {
    object: Object<ObjectCore>,
    seller: address,
    delete_ref: DeleteRef,
    extend_ref: ExtendRef,
}

#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct FixedPriceListing<phantom CoinType> has key {
    price: u64,
}

There is a small difference when initializing objects using object::create_object_from_account(account) for resource groups compared to regular resources. However, once initialized, accessing the values is the same, with the added benefits of better storage efficiency and performance from using resource groups.

Aptos Standard

Objects

Objects in Aptos are versatile containers for resources, enabling complex data structures on-chain. They possess unique addresses and can own resources, similar to accounts.

Categories of objects

  • Normal Object:

    • This type is deletable, meaning it can be removed from the blockchain.

    • It has a random address, so its location on the blockchain is not predictable.

  • Named Object:

    • This type is not deletable, meaning once created, it cannot be removed from the blockchain.

    • It has a deterministic address, meaning its location on the blockchain can be calculated or predicted based on certain parameters (like a name or seed).

  • Sticky Object:

    • Like the named object, this type is also not deletable.

    • However, unlike the named object, it has a random address similar to the normal object.

Characteristics of Objects

Ownership: Separation of ownership and data location

  • All objects are globally accessible, with references stored inside the object.

  • The model allows easy transfer of ownership.

  • Objects can be transferable, burnable, or extendable.

  • The defining module controls permissions and ownership rules.

  • Account-based storage keeps resources within accounts for efficient parallel execution.

Capabilities: These define how the object can be manipulated. These capabilities are implemented through "Refs" that are generated during object creation. There are 4 main types of object capabilities:

  1. Delete Capability (DeleteRef)

    • Allows removing an object from the blockchain, useful for cleaning up data and possibly getting a storage refund.

    • Created using object::generate_delete_ref(&constructor_ref)

    • Only for deletable objects (not for named or sticky objects).

    • Lets the holder delete the object with object::delete(delete_ref).

  2. Extend Capability (ExtendRef)

    • Lets you add new resources to an existing object after it's created.

    • Created using object::generate_extend_ref(&constructor_ref).

    • Allows creating a signer for the object to add new resources.

    • Useful for upgradeable or extensible objects.

  3. Transfer Capability (TransferRef)

    • Controls the transfer of an object between addresses or to other objects.

    • Created using object::generate_transfer_ref(&constructor_ref).

    • Can enable or disable ungated transfers.

    • Allows creating one-time use LinearTransferRef for controlled transfers.

  4. Create Signer Capability

    • Allows generating a signer for the object, crucial for adding resources or performing actions that need authorization.

    • Available through the ConstructorRef.

    • Used with object::generate_signer(&constructor_ref).

    • Essential for initializing the object with resources during creation.

Composability: The Move object data model supports flexible and expandable token designs.

  • Enhance NFT features by allowing objects to own other objects, making it easier to create complex digital assets.

  • This is especially useful for gaming assets, enabling NFTs to evolve, combine, and be customized.

  • METAPIXEL demonstrates that game assets can be minted as NFTs, made soul-bound, and upgraded in real-time, all within a single token object.

Digital Asset (DA)

The Digital Asset (DA) standard is a modern Non-Fungible Token (NFT) framework for Aptos, replacing the old Aptos Token Standard. The DA standard has two main components:

  • Collections: Sets of NFTs with shared context.

  • Tokens: Digital assets representing unique items.

Before creating individual NFTs, you need to set up a collection. Some key features are:

  • Add extra data and features to tokens without changing the core system.

  • Transfer ownership by updating the reference.

  • Transfer directly without the recipient needing to opt-in first.

  • Support for NFT hierarchies, allowing one NFT to own others, making things more flexible.

  • Supports soul-bound tokens, which are non-transferable assets tied to a specific address.

Fungible Asset (FA)

A fungible asset is a type of asset that can be exchanged and is identical to other assets of the same kind, like currency.

It provides a type-safe way to create customizable fungible assets using Move Objects. It is a modern replacement for the coin module, offering seamless minting, transfer, and customization of fungible assets for various use cases.

FAs use two main objects:

  • Metadata: Stores asset details (name, symbol, decimals)

  • FungibleStore: Tracks balances for each holder

Aptos Fungible Asset (FA) Standard | Aptos Docs (en)

The metadata of the fungible asset is stored separately, while balances are kept in main or secondary storage objects owned by accounts. This allows for quick transfers and flexible balance management across accounts or purposes.

Within the Fungible Asset framework, the Primary Fungible Store is essential:

  • It provides permanent storage for each type of Fungible Asset (FA) linked to an account.

  • The primary storage is created automatically when a fungible asset is transferred to a new account.

  • Each account has one Primary Fungible Store per FA type, simplifying asset management.

  • Users can create Secondary Stores with random addresses, which can be deleted when empty.

  • Interaction with Primary Fungible Stores is done through functions in the primary_fungible_store.move module.

  • Secondary storage allows for different balances of the same asset type within one account, similar to having different bank accounts for the same currency.

This standard improves on the previous coin module by offering more customization and automatic balance tracking for recipients.


Build a Dapp

Quick Start

Using the create-aptos-dapp tool, you can set up a basic project for a decentralized application (dapp). Run the following command in your terminal:

npx create-aptos-dapp

It will open the wizard to set up the project. Choose "Token minting dapp."

Run npm run move:init to set up an account for publishing the Move contract. This command will generate a .aptos/config.yaml file containing the account's private key, address, and network settings.

Go to the .env file and set VITE_FA_CREATOR_ADDRESS to the generated address in .aptos/config.yaml.

VITE_FA_CREATOR_ADDRESS="PASTE YOUR ADDRESS HERE"

Publish

Run the following command to create a new object and publish the Move package to it on the Aptos blockchain.

The my-aptos-dapp-testnet is the profile name from .aptos/config.yaml.

Update the myseed123 seed to publish to a new address.

aptos move create-resource-account-and-publish-package \
  --package-dir move \
  --address-name launchpad_addr \
  --named-addresses initial_creator_addr=my-aptos-dapp-testnet,launchpad_addr=my-aptos-dapp-testnet \
  --profile my-aptos-dapp-testnet \
  --skip-fetch-latest-git-deps \
  --assume-yes \
  --seed myseed123 \

This command creates a resource account and publishes a Move package under that account's address:

aptos move create-resource-account-and-publish-package: This is the main command that creates a resource account and publishes a Move package in one operation.

  • --package-dir move: Specifies the directory containing the Move package to be published.

  • --named-addresses initial_creator_addr=my-aptos-dapp-testnet,launchpad_addr=my-aptos-dapp-testnet: Sets named addresses used in the Move code. This allows for flexibility in deployment by replacing placeholder addresses with actual account addresses.

  • --skip-fetch-latest-git-deps: Skips fetching the latest dependencies from Git repositories.

  • --profile my-aptos-dapp-testnet: Specifies the profile to use for this operation. Profiles in Aptos CLI store configuration settings, including account information.

  • --address-name launchpad_addr: Specifies the name of the address to use for the resource account.

  • --seed myseed123: Provides a seed for generating the resource account. Resource accounts are created based on the SHA3-256 hash of the source address and this additional seed data.

  • --assume-yes: Automatically confirms any prompts during the execution of the command.

After running the command, the module gets published on Testnet. Now, we want to know where it has been deployed. Open the transaction in the explorer to find out.

In the explorer, go to the events tab and copy the code_address. This is our module address.

Go to the .env file, add VITE_MODULE_ADDRESS, and paste the module address you copied from the explorer.

VITE_MODULE_ADDRESS="MODULE ADDRESS HERE"

Install Petra Aptos Wallet

Next, we will work on the frontend, which needs a wallet to connect to the web app and interact with it. Petra is the official wallet made by the Aptos team and is highly recommended.

If you don't have it, click on the Petra Wallet extension to install it.

You can find the private key in .aptos/config.yaml to use the same account in the browser. Make sure to switch to Testnet in the settings.

Create and Mint Token

Run the frontend with the following command

npm run dev

Go to Create Asset and fill out the form. To keep it simple, you can use my image. Right-click the image and select "Save Image As..." to download it. Remember, the image size is important. Images are uploaded to Irys provenance layer for permanent storage. The image below is 250kb. I tried with a 5mb image before, and 0.9 APT wasn't enough.

A golden, intricately designed flower with layered petals on a gold background.

Now we are ready to mint. Go to the My Assets page and copy the FA address. This is our asset_id that we need to use for minting.

Open your code editor and paste it into the frontend frontend/config.ts file.

Reload the webpage, go to the "Mint Page," and mint your own token on Aptos.

Great! Now that we know how to publish, create a token, and mint it, it's time to go further and update our code to add an allowlist. This will ensure that only addresses on the list can mint the token, and only the token creator can add addresses to the list.

Add Allowlist

Update Module

To add an allowlist, follow these steps:

  1. Open your code editor and navigate to the module file.

  2. Add the necessary code to create an allowlist. This list will contain the addresses that are allowed to mint the token.

  3. Ensure that only the token creator has the permission to add addresses to this allowlist.

  4. Save your changes and publish the updated module.

First, let's add a new struct designed to store a list of addresses.

struct Allowlist has key {
    addresses: vector<address>,
}

We'll update the Config struct to include the allowlist.

    struct Config has key {
        // ... existing code ...
        allowlist: Allowlist, // <- Add the allowlist
    }

Let's add a function to initialize an empty allowlist in the init_module function.

fun init_module(sender: &signer) {
    // ... existing code ...
    move_to(sender, Config {
        // ... existing code ...
        allowlist: Allowlist { addresses: vector::empty() }, // <- New
    });
}

Now, let's add a function that implements a controlled way to add addresses to an allowlist, ensuring that only the creator of the allowlist.

public entry fun add_allowlist(sender: &signer, address_to_add: address) acquires Config {
    let sender_addr = signer::address_of(sender);
    let config = borrow_global_mut<Config>(@launchpad_addr);
    assert!(sender_addr == config.creator_addr, EONLY_CREATOR_CAN_ADD_TO_ALLOWLIST);
    vector::push_back(&mut config.allowlist.addresses, address_to_add);
}

We will add a view function to get the list of addresses in the allowlist from a config stored on the blockchain.

#[view]
public fun get_allowlist(): vector<address> acquires Config {
    let config = borrow_global<Config>(@launchpad_addr);
    config.allowlist.addresses
}

By using the #[view] decorator, it ensures that this function can be called to read data without incurring gas costs or modifying the blockchain state

Now, let's modify the mint_fa function to check if the sender is in the allowlist.

public entry fun mint_fa(
    sender: &signer,
    fa_obj: Object<Metadata>,
    amount: u64
) acquires FAController, FAConfig, Config {
    let sender_addr = signer::address_of(sender);
    let config = borrow_global<Config>(@launchpad_addr); // New
    assert!(vector::contains(&config.allowlist.addresses, &sender_addr), ENOT_IN_ALLOWLIST); // New
    check_mint_limit_and_update_mint_tracker(sender_addr, fa_obj, amount);
    let total_mint_fee = get_mint_fee(fa_obj, amount);
    pay_for_mint(sender, total_mint_fee);
    mint_fa_internal(sender, fa_obj, amount, total_mint_fee);
}

Let's add a new error code.

const EONLY_CREATOR_CAN_ADD_TO_ALLOWLIST: u64 = 8;
const ENOT_IN_ALLOWLIST: u64 = 9;

Finally, let's update the tests:

#[test(aptos_framework = @0x1, sender = @launchpad_addr)]
fun test_happy_path(
    aptos_framework: &signer,
    sender: &signer,
) acquires Registry, FAController, Config, FAConfig {
    let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(aptos_framework);

    let sender_addr = signer::address_of(sender);

    init_module(sender);

    add_allowlist(sender, sender_addr); // Add This

    // ... existing code ...
}

and add a new test to verify it fails if the minter address is not added to the allowlist

#[test(aptos_framework = @0x1, sender = @launchpad_addr, minter = @0x123)]
#[expected_failure(abort_code = 9, location = Self)]
fun test_not_allowlist(
    aptos_framework: &signer,
    sender: &signer,
    minter: &signer
) acquires Registry, FAController, Config, FAConfig {
    let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(aptos_framework);
    init_module(sender);
    // Create FA
    create_fa(
        sender,
        option::some(1000),
        string::utf8(b"FA1"),
        string::utf8(b"FA1"),
        2,
        string::utf8(b"icon_url"),
        string::utf8(b"project_url"),
        option::none(),
        option::none(),
        option::some(500)
    );
    let registry = get_registry();
    let fa_1 = *vector::borrow(&registry, vector::length(&registry) - 1);
    // Try to mint as minter (should fail)
    mint_fa(minter, fa_1, 20);
    coin::destroy_burn_cap(burn_cap);
    coin::destroy_mint_cap(mint_cap);
}

You can see that we expect the test to fail with ENOT_IN_ALLOWLIST using #[expected_failure(abort_code = 9, location = Self)].

Test Allowlist

Let's test our update module.

aptos move test \
  --package-dir move \
  --named-addresses initial_creator_addr=my-aptos-dapp-testnet,launchpad_addr=my-aptos-dapp-testnet \
  --skip-fetch-latest-git-deps

if everything are implemented correctly, you should confirm with the following result.

Running Move unit tests
[ PASS    ] 0xc35877ebfabf135afd87e8dfd967fb42a29e3daab4893005cbfbf4a5fa895429::launchpad::test_allowlist
[ PASS    ] 0xc35877ebfabf135afd87e8dfd967fb42a29e3daab4893005cbfbf4a5fa895429::launchpad::test_happy_path
Test result: OK. Total tests: 2; passed: 2; failed: 0
{
  "Result": "Success"
}

Publish again with a different seed and repeat the process of updating the module address. Remove the asset_id to avoid the MISSING_DATA "can't borrow" error.

Update Frontend

Create a new file AllowlistPage.tsx for the allowlist page under the pages folder.

// Internal components
import { LaunchpadHeader } from "@/components/LaunchpadHeader";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
// Internal hooks
import { useAddToAllowlist } from "@/hooks/useAddToAllowlist";
import { useGetAllowlist } from "@/hooks/useGetAllowlist";
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { useState } from "react";

export function AllowlistPage() {
  const [newAddress, setNewAddress] = useState("");
  const { account } = useWallet();
  const { mutate: addToAllowlist, isPending: isAdding } = useAddToAllowlist();
  const { data: allowlist, isPending: isLoadingAllowlist } = useGetAllowlist();

  const handleAddAddress = () => {
    if (newAddress && account) {
      addToAllowlist({ address: newAddress });
      setNewAddress("");
    }
  };

  return (
    <>
      <LaunchpadHeader title="Allowlist Management" />
      <div className="container mx-auto p-4">
        <div className="mb-4">
          <input
            type="text"
            value={newAddress}
            onChange={(e) => setNewAddress(e.target.value)}
            placeholder="Enter address to add"
            className="border p-2 mr-2"
          />
          <button
            onClick={handleAddAddress}
            disabled={isAdding || !account}
            className="bg-blue-500 text-white p-2 rounded"
          >
            {isAdding ? "Adding..." : "Add to Allowlist"}
          </button>
        </div>
        <Table>
          <TableCaption>Current Allowlist</TableCaption>
          <TableHeader>
            <TableRow>
              <TableHead>Address</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {isLoadingAllowlist ? (
              <TableRow>
                <TableCell colSpan={1}>Loading allowlist...</TableCell>
              </TableRow>
            ) : (
              allowlist?.map((address, index) => (
                <TableRow key={index}>
                  <TableCell>{address}</TableCell>
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </div>
    </>
  );
}

We can see that the file includes two hooks to get and add to the allowlist. Let's define those hooks.

const { mutate: addToAllowlist, isPending: isAdding } = useAddToAllowlist();
const { data: allowlist, isPending: isLoadingAllowlist } = useGetAllowlist();

Under the entry-functions folder, add add_allowlist.ts.

import { InputTransactionData } from "@aptos-labs/wallet-adapter-react";
import { MODULE_ADDRESS } from "@/constants";

export type AddToAllowlistArguments = {
  address: string;
};

export const addToAllowlist = (args: AddToAllowlistArguments): InputTransactionData => {
  const { address } = args;
  return {
    data: {
      function: `${MODULE_ADDRESS}::launchpad::add_allowlist`,
      typeArguments: [],
      functionArguments: [address],
    },
  };
};

Create a new hook called useAddToAllowlist.ts in the hooks folder.

import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { addToAllowlist, AddToAllowlistArguments } from "@/entry-functions/add_allowlist";

export function useAddToAllowlist() {
  const { signAndSubmitTransaction } = useWallet();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (args: AddToAllowlistArguments) => {
      const transaction = addToAllowlist(args);
      const result = await signAndSubmitTransaction(transaction);
      return result;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["allowlist"] });
    },
  });
}

and also create a new hook in useGetAllowlist.ts:

import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { useQuery } from "@tanstack/react-query";
import { aptosClient } from "@/utils/aptosClient";
import { MODULE_ADDRESS } from "@/constants";

export function useGetAllowlist() {
  const { account } = useWallet();

  return useQuery({
    queryKey: ["allowlist"],
    enabled: !!account,
    queryFn: async () => {
      const result = await aptosClient().view({
        payload: {
          function: `${MODULE_ADDRESS}::launchpad::get_allowlist`,
          typeArguments: [],
          functionArguments: [],
        },
      });
      return result[0] as string[];
    },
  });
}

Update the App.tsx routing configuration to include the new AllowlistPage component.

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      {
        path: "/",
        element: <Mint />,
      },
      {
        path: "create-asset",
        element: <CreateFungibleAsset />,
      },
      {
        path: "my-assets",
        element: <MyFungibleAssets />,
      },
      {
        path: "allowlist",
        element: <AllowlistPage />,
      },
    ],
  },
]);

Update Header.tsx to include the new navigation link.

<Link className={buttonVariants({ variant: "link" })} to={"/allowlist"}>
    Allowlist
</Link>

Now, follow the same steps as before to create an asset and try to mint it. However, this time you should see the error: ENOT_IN_ALLOWLIST.

Go to the allowlist page and add your address to the list.

If you try to mint again with the allowed address, it should go through this time. Now, only those on the list can mint the token. But why have an allowlist? There are many use cases to incentivize users to mint, such as exclusive access to a community or content, for example.

Add Token Gate

Let's update our frontend with an exclusive page that only those who have minted the token can access.

Create a new TokenGatedPage.tsx file under the pages folder.

import { LaunchpadHeader } from "@/components/LaunchpadHeader";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useGetAssetData } from "@/hooks/useGetAssetData";
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { useEffect, useState } from "react";

export function TokenGatedPage() {
  const { account } = useWallet();
  const [hasAccess, setHasAccess] = useState(false);
  const { data } = useGetAssetData();
  const { asset, totalAbleToMint = 0, yourBalance = 0 } = data ?? {};

  useEffect(() => {
    if (yourBalance && yourBalance >= 1) {
      setHasAccess(true);
    } else {
      setHasAccess(false);
    }
  }, [yourBalance]);

  if (!account) {
    return (
      <div className="container mx-auto p-4 text-center">
        <p className="text-xl">Please connect your wallet to access this page.</p>
      </div>
    );
  }

  if (!data) {
    return (
      <div className="container mx-auto p-4 text-center">
        <p className="text-xl">Loading asset data...</p>
      </div>
    );
  }

  if (!hasAccess) {
    return (
      <>
        <LaunchpadHeader title="Exclusive Content" />
        <div className="container mx-auto p-4 text-center min-h-4/5 flex items-center justify-center flex-col">
          <div className="p-10 shadow-lg rounded-lg mt-10">
            <p className="text-xl">You need at least 1 token to access this page.</p>
            <p className="mt-2">
              Your current balance: {yourBalance} {asset?.symbol}
            </p>
            <p className="mt-4 text-3xl">😢</p>
            <Button onClick={() => (window.location.href = "/")} className="mt-4">
              Go Back to Home
            </Button>
          </div>
        </div>
      </>
    );
  }

  return (
    <>
      <LaunchpadHeader title="Exclusive Content" />
      <div className="container mx-auto p-4">
        <h2 className="text-2xl font-bold mb-4">Welcome to the exclusive area! 😎🏖️</h2>
        <Table>
          <TableCaption>Asset Information</TableCaption>
          <TableHeader>
            <TableRow>
              <TableHead>Property</TableHead>
              <TableHead>Value</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            <TableRow>
              <TableCell>Asset Name</TableCell>
              <TableCell>{asset?.name}</TableCell>
            </TableRow>
            <TableRow>
              <TableCell>Asset Symbol</TableCell>
              <TableCell>{asset?.symbol}</TableCell>
            </TableRow>
            <TableRow>
              <TableCell>Your Balance</TableCell>
              <TableCell>{yourBalance}</TableCell>
            </TableRow>
            <TableRow>
              <TableCell>Total Able to Mint</TableCell>
              <TableCell>{totalAbleToMint}</TableCell>
            </TableRow>
          </TableBody>
        </Table>
      </div>
    </>
  );
}

We use yourBalance to check if the user holds any tokens and show different content based on the balance. This value comes from the hook useGetAssetData, which uses aptosClient().queryIndexer(...) to fetch the balance of a specific account for the given asset.

current_fungible_asset_balances(
  where: {owner_address: {_eq: $account}, asset_type: {_eq: $asset_id}}
  distinct_on: asset_type
  limit: 1
) {
  amount
}

Let's add the page to the router

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      {
        path: "/",
        element: <Mint />,
      },
      {
        path: "create-asset",
        element: <CreateFungibleAsset />,
      },
      {
        path: "my-assets",
        element: <MyFungibleAssets />,
      },
      {
        path: "allowlist",
        element: <AllowlistPage />,
      },
      {
        path: "token-gated",
        element: <TokenGatedPage />,
      },
    ],
  },
]);

and update the header to include the token-gated page.

<div className="flex gap-2 items-center flex-wrap">
  <Link
    className={`${buttonVariants({ variant: "link" })} animate-pulse bg-gradient-to-r from-blue-500 to-green-500`}
    to={"/token-gated"}
  >
    VIP
  </Link>
  {IS_DEV && (
    <>
      <Link className={buttonVariants({ variant: "link" })} to={"/my-assets"}>
        My Assets
      </Link>
      <Link className={buttonVariants({ variant: "link" })} to={"/create-asset"}>
        Create Asset
      </Link>
      <Link className={buttonVariants({ variant: "link" })} to={"/allowlist"}>
        Allowlist
      </Link>
    </>
  )}

  <WalletSelector />
</div>

Now open Petra, create a new account without the token, and try to access the VIP page. You will see...

Oh no, you are not special. But if you switch back to the account with the token, you will load a different page, just for you, because you have the token in your account.

Whew, that was intense! Congrats, you made it.


What’s Next?

On building your first Fungible Asset minting dapp on Aptos with allowlist and token-gated access. From here, the sky's the limit! There are so many cool things you can explore and add to your project. Here are some fun ideas to get you started:

  • Tiered Minting Stages: Implement multiple minting phases with different prices or requirements, such as an early access phase for allowlist members followed by a public sale.

  • Dynamic Pricing: Create a minting mechanism where the price adjusts based on demand or the time elapsed since the start of the sale.

  • Staking Mechanisms: Develop a staking system where users can lock up their tokens to earn rewards or gain additional benefits.

  • Integration with Other Protocols: Consider how your Fungible Asset could interact with other protocols or standards within the Aptos ecosystem.

Community

Need a hand or just want to chat with other Aptos developers? Here are two awesome places to check out:

GitHub Discussions: The Aptos Labs team has a super active GitHub Discussions forum where you can ask questions and join in on conversations. Check it out here: https://github.com/aptos-labs/aptos-developer-discussions/discussions

Discord Community: For real-time help and to hang out with the community, the Aptos Discord server is the place to be: https://discord.com/invite/aptosnetwork

Learning Resources

https://github.com/aeither/aptos-allowlist-token

https://www.youtube.com/watch?v=YekoFYYehxo

https://learn.aptoslabs.com/code-example/

https://aptos.dev/