Skip to main content

What is Nifty Asset Standard and How to Mint Your First Nifty Asset?

Updated on
Aug 13, 2024

16 min read

Overview​

As excitement for digital assets and DeFi continues to grow, so do use cases for digital assets on the blockchain. Whether tokenizing real-world assets (RWAs), minting digital art, selling in-game assets, managing club memberships, or other digital goods/services, you need tools that make it efficient and affordable to create, manage, and interact with digital assets on the blockchain.

Nifty OSS is a new set of Open Source protocols on Solana that provide lightweight tooling and efficient standards for managing digital assets. Like Metaplex Core, Nifty Asset is a single-account digital asset standard that significantly reduces the storage costs, compute requirements, and account requirements for creating and managing digital assets on Solana.

In this guide, we will create and experiment with a digital asset using the Nifty JavaScript client.

Prerequisites​

Before we dive in, make sure you have the following installed:

Dependencies Used in this Guide​

DependencyVersion
@metaplex-foundation/umi^0.9.1
@metaplex-foundation/umi-bundle-defaults^0.9.1
@nifty-oss/asset^0.5.0
solana cli1.18.8

What You Will Do​

This guide will walk you through setting up your local development environment to use Nifty Asset. We will create a sample application to demonstrate its capabilities. Specifically, we will cover the following functionalities:

  • Minting: Create new digital assets.
  • Grouping: Organize assets into groups.
  • Attributes and Extensions: Add metadata and functionalities to assets.
  • Delegation and Locking: Manage permissions and lock assets.
  • Transfers: Transfer ownership of assets.

Let's get started!

Understanding Nifty Asset​

Most Traditional NFT standards on Solana are built on top of the SPL Token program, which was originally designed for fungible tokens. This approach requires NFTs to adhere to data requirements for fungible tokens, such as having a token supply and decimals, leading to inefficiencies in development and storage.

Nifty Asset takes a new approach to representing non-fungible tokens on Solana. Instead of using multiple accounts, it treats an NFT as a unique slab of bytes identified by an address with an associated holder. This approach minimizes account usage, requiring only a single account to represent an asset. By optimizing compute unit consumption and using zero-copy (bytemuck) techniques to avoid serialization overheads, Nifty Asset offers a lightweight, efficient, and flexible solution for managing NFTs on the Solana blockchain. This new standard enhances performance and composability, making creating and interacting with digital assets easier. Nifty advertises the following features:

It has the following on-chain features enabled through extensions:

  • Traits and attributes
  • Generic blob data (image, gif, etc.)
  • Metadata
  • Royalty enforcement
  • Grouping/collections
  • Pointer/Links to off-chain data
  • A delegate system with various roles
  • Locking and unlocking of assets

Asset Account Data Structure​

Every Asset includes: -Β 168 bytes of base metadata. -Β 16 bytes for each extension. -Β A variable number of bytes for the extension data (based on the extension type and underlying data).

The base metadata includes the following fields:

FieldDescription
DiscriminatorRepresents the type of account. Currently: Unitialized or Asset
StateThe state of the asset: Locked or Unlocked
StandardThe type of Nifty Asset - current options: NonFungible, Subscription and Soulbound
MutableWhether or not the asset can be modified
OwnerThe owner of the asset. Owners can approve delegates, transfer, and burn the asset.
GroupWhat group, if any, the asset belongs to.
AuthorityThe authority of the asset. Authorities can update assets and verify them as a member of a group.
DelegateThe delegate of the asset. Delegates can burn, lock or transfer the asset, depending on the roles assigned to them.
NameThe name of the asset, up to 35 characters in length.

source: Nifty OSS Docs

Note that the owner is defined in the asset account, rather than a separate linked account (like an SPL token account). This allows for more efficient storage and management of assets.


Unaudited Program

The Nifty Asset standard is still in development and has yet to be audited. Use caution when deploying to mainnet and ensure you have thoroughly tested your application on devnet or testnet before deploying to mainnet. Check their GitHub for the latest updates and documentation.

Setting Up the Project​

Download Necessary Programs​

We will be running our tests today on a local validator. Because the Asset program is not included in your local validator, we will need to download it from the network to initiate them when we start our local validator.

solana program dump -um AssetGtQBTSgm5s91d1RAQod5JmaZiJDxqsgtqrZud73 nifty_asset.so

This command will write the program executable data to nifty_asset.so in your project directory. We will use it later when we are ready to run our script.

Initialize a New Node.js Project​

First, create a new directory for your project and initialize a Node.js project.

mkdir nifty-asset-project
cd nifty-asset-project
npm init -y

Install Dependencies​

Next, install the necessary dependencies:

npm install @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @nifty-oss/asset

Create the Project Files​

Create a new file named index.ts in your project directory.

echo > index.ts

Import Dependencies​

First, import the required modules and set up the UMI instance and signers. Add the following code to the index.ts file:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import {
TransactionBuilderSendAndConfirmOptions,
generateSigner,
keypairIdentity,
sol
} from '@metaplex-foundation/umi';
import {
DelegateRole,
approve,
attributes,
creators,
delegateInput,
grouping,
lock,
mint,
niftyAsset,
revoke,
royalties,
transfer,
unlock,
verify
} from '@nifty-oss/asset';

The createUmi function initializes the Umi client with the default options. Umi is a Solana client library that provides a high-level API for interacting with the Solana blockchain. The Nifty Asset library includes functions for minting, transferring, and managing digital assets--we will discuss these functions in detail later.

Initialize UMI and Signers​

Next, initialize the Umi client and create signers for creator, owner, asset, groupAsset (aka collection), and delegate accounts. We will also set the default options for sending and confirming transactions (we can use processed here since we are just testing on our local host).

Add the following code to the index.ts file:

const umi = createUmi('http://127.0.0.1:8899', { commitment: 'processed' }).use(niftyAsset());

const creator = generateSigner(umi);
const owner = generateSigner(umi);
const asset = generateSigner(umi);
const groupAsset = generateSigner(umi);
const delegate = generateSigner(umi);

umi.use(keypairIdentity(creator));

const options: TransactionBuilderSendAndConfirmOptions = {
confirm: { commitment: 'processed' }
};

Writing the Sample Application​

Let's break down the code into sections to explain each function and then tie it all together with a main() function.

Airdropping SOL​

First, we will need to make sure that our creator, owner, and delegate wallets have SOL to pay for transaction fees and rent. Add the following function to index.ts to airdrop SOL to these accounts:

async function airdropFunds() {
try {
await umi.rpc.airdrop(creator.publicKey, sol(100), options.confirm);
await umi.rpc.airdrop(owner.publicKey, sol(100), options.confirm);
await umi.rpc.airdrop(delegate.publicKey, sol(100), options.confirm);
console.log(`1. βœ… - Airdropped 100 SOL to the ${creator.publicKey.toString()}`);
} catch (error) {
console.error('1. ❌ - Error airdropping SOL to the wallet.', error);
}
}

Notice we are putting our function in a try-catch block to handle any errors that may occur during the airdrop process. We are logging βœ… when our expected outcome occurs and ❌ if it does not -- we will use this pattern throughout the code to track the progress of our application.

Minting a Group Asset​

Let's start by creating a groupAsset, which will be an asset that manages the association of a group of assets. Add the following function to index.ts to mint a new group asset:

async function mintGroupAsset() {
try {
await mint(umi, {
asset: groupAsset,
payer: umi.identity,
name: 'Group',
extensions: [
grouping(10),
creators([{ address: creator.publicKey, share: 100 }]),
royalties(5)
],
}).sendAndConfirm(umi, options);

await verify(umi, {
asset: groupAsset.publicKey,
creator,
}).sendAndConfirm(umi);

console.log(`2. βœ… - Minted a new Group Asset: ${groupAsset.publicKey.toString()}`);
} catch (error) {
console.error('2. ❌ - Error minting a new Group Asset.', error);
}
}

Let's break this down a bit because we are doing a few things here:

  1. First, we use the mint function to create a new asset. We are passing in the groupAsset signer to define the asset account address. We are applying three extensions to the asset:


    • grouping(10): Sets a maximum group size.
    • creators([{ address: creator.publicKey, share: 100 }]): Defines the creator of the asset and their share (the total must be 100%).
    • royalties(5): Sets the royalties percentage (in this case, 5%).

    The creator share and royalties field will be used to distribute the royalties to the creators of any assets in the group (meaning we do not need to use account space in our individual assets to store this information).

  2. Next, we use the verify function to verify the creator of the asset. This prevents somebody from minting an asset and claiming to be the creator when they are not. After verifying the creator, a boolean in the account is set to true, which can be checked by other programs to verify the creator of the asset.

Minting a Digital Asset​

Now that we have created a group let's mint a new digital asset and add it to the group. Add the following function to index.ts to mint a new digital asset:

async function mintAsset() {
try {
await mint(umi, {
asset,
owner: owner.publicKey,
authority: creator.publicKey,
payer: umi.identity,
group: groupAsset.publicKey,
name: 'Digital Asset1',
extensions: [
attributes([{ name: 'head', value: 'hat' }]),
]
}).sendAndConfirm(umi, options);
console.log(`3. βœ… - Minted a new Asset: ${asset.publicKey.toString()}`);
} catch (error) {
console.error('3. ❌ - Error minting a new NFT.', error);
}
}

Since we are using the same signer/authority to create the asset as the group, we can add the asset to the group by passing in the group parameter. If we were using a different signer/authority, we would need to use the group function from the nifty-asset library to add the asset to the group.

We are also adding an attributes extension to the asset, which will allow us to add metadata to it. In this case, we are adding a trait named head with a value of hat as an example. Note that in this case, we want our extensions to be unique to the assetβ€”we do not need to add the creators or royalties extensions since they are already defined in the group asset.

Approving a Delegate​

Now, let's experiment with some of the native functions of the Nifty Asset standard. We will assign a delegate to the asset and give them the authority to lock and unlock the asset. Add the following function to index.ts to approve a delegate:

async function approveDelegate() {
try {
await approve(umi, {
asset: asset.publicKey,
owner,
delegate: delegate.publicKey,
delegateInput: delegateInput('Some', {
roles: [DelegateRole.Lock],
}),
}).sendAndConfirm(umi, options);
console.log(`4. βœ… - Assigned Delegate Lock Authority for the Asset`);
} catch (error) {
console.error('4. ❌ - Error assigning Delegate Lock Authority for the Asset.', error);
}
}

We use the approve function and pass a delegateInput object to define the delegate's roles. In this case, we assign the Lock role to the delegate, allowing them to lock and unlock the asset (other options include Transfer, Burn, and None). We are also passing the owner signer to approve the delegate.

Locking the Asset​

Now, let's create a function to lock the asset using the delegate. This will prevent the asset from being transferred. Add the following function to index.ts to lock the asset:

async function lockAsset() {
try {
await lock(umi, {
asset: asset.publicKey,
signer: delegate,
}).sendAndConfirm(umi, options);
console.log(`5. βœ… - Locked the Asset: ${asset.publicKey.toString()}`);
} catch (error) {
console.error('5. ❌ - Error locking the Asset.', error);
}
}

The nifty/asset library provides a lock function to lock the asset. We are passing the delegate signer to lock the asset. This will prevent the asset from being transferred until it is unlocked. Let's see what happens when we try to transfer the locked asset.

Attempting to Transfer a Locked Asset​

Let's add a transfer function that will attempt to send the asset to a new recipient. Since the asset is locked, the transfer should fail. Add the following function to index.ts to try to transfer the locked asset:

async function tryTransferLockedAsset() {
try {
await transfer(umi, {
asset: asset.publicKey,
signer: owner,
recipient: generateSigner(umi).publicKey,
group: groupAsset.publicKey
}).sendAndConfirm(umi, options);
console.log(`6. ❌ - Asset should not have been transferred as it is locked.`);
} catch (error) {
console.log('6. βœ… - Asset Cannot be transferred as it is locked.');
}
}

Unlocking the Asset​

Let's create a function that unlocks the asset using the unlock function. Add the following function to index.ts to unlock the asset:

async function unlockAsset() {
try {
await unlock(umi, {
asset: asset.publicKey,
signer: delegate,
}).sendAndConfirm(umi, options);
console.log(`7. βœ… - Unlocked the Asset: ${asset.publicKey.toString()}`);
} catch (error) {
console.error('7. ❌ - Error unlocking the Asset.', error);
}
}

Note that we must use the same delegate signer with the lock authority to unlock the asset.

Revoking the Delegate​

Now, the asset should be unlocked. Before we attempt to transfer it, let's revoke the delegate's lock authority to prevent any unintended restrictions on the asset. Add the following function to index.ts to revoke the delegate:

async function revokeDelegate() {
try {
await revoke(umi, {
asset: asset.publicKey,
signer: owner,
delegateInput: delegateInput('Some', {
roles: [DelegateRole.Lock],
}),
}).sendAndConfirm(umi, options);
console.log(`8. βœ… - Revoked the Delegate Lock Authority for the Asset`);
} catch (error) {
console.error('8. ❌ - Error revoking the Delegate Lock Authority for the Asset.', error);
}
}

The revoke function serves a purpose opposite to the approve function. We are passing the owner signer to revoke the delegate's lock authority.

Transferring the Asset​

Finally, now that the delegate has been removed, let's try to transfer the asset to a new recipient. Add the following function to index.ts to transfer the asset:

async function transferAsset() {
try {
await transfer(umi, {
asset: asset.publicKey,
signer: owner,
recipient: generateSigner(umi).publicKey,
group: groupAsset.publicKey
}).sendAndConfirm(umi, options);
console.log(`9. βœ… - Transferred the Asset: ${asset.publicKey.toString()}`);
} catch (error) {
console.error('9. ❌ - Error transferring the Asset.', error);
}
}

We are using the transfer function to transfer the asset to a new recipient. We are passing the owner signer to transfer the asset to a newly generated account. Note that we must also pass the group parameter to ensure that royalties enforcement is applied if applicable.

Main Function​

Create a main() function that ties all the individual functions together to execute the entire process sequentially. Add the following to the end of the index.ts file:

async function main() {
await airdropFunds();
await mintGroupAsset();
await mintAsset();
await approveDelegate();
await lockAsset();
await tryTransferLockedAsset();
await unlockAsset();
await revokeDelegate();
await transferAsset();
}

main();

Running the Code​

To run your script, start your local validator with the Nifty Asset program we downloaded earlier. Make sure you run this from the same directory that the files are located in (or provide the full path to the files):

solana-test-validator -r --bpf-program AssetGtQBTSgm5s91d1RAQod5JmaZiJDxqsgtqrZud73 nifty_asset.so

Your validator should start with the programs loaded. If you have any trouble, check out our Solana Validator Setup Guide for more information, or, if you prefer, you can use a free Solana Devnet endpoint from QuickNode.com.

To run the code, execute the following command in your terminal:

ts-node index.ts

If everything is set up correctly, you should see console logs indicating the successful execution of each step or detailed error messages if something goes wrong.

ts-node index.ts
1. βœ… - Airdropped 100 SOL to the 3qmxo6SBygjbwj4mZZ5ivw7XEniXXSt6m6MGqVWacCvt
2. βœ… - Minted a new Group Asset: Auma2yBkHa59SKeWT9ru31KGYc8TrC8V8xwMW7pmTNgj
3. βœ… - Minted a new Asset: FKGAfNdmGEPfqb2j7HiDmTfMurVPmWY8uoP2v2viyB1
4. βœ… - Assigned Delegate Lock Authority for the Asset
5. βœ… - Locked the Asset: FKGAfNdmGEPfqb2j7HiDmTfMurVPmWY8uoP2v2viyB1
6. βœ… - Asset Cannot be transferred as it is locked.
7. βœ… - Unlocked the Asset: FKGAfNdmGEPfqb2j7HiDmTfMurVPmWY8uoP2v2viyB1
8. βœ… - Revoked the Delegate Lock Authority for the Asset
9. βœ… - Transferred the Asset: FKGAfNdmGEPfqb2j7HiDmTfMurVPmWY8uoP2v2viyB1

Let's go!!! Pretty nifty, huh?

Keep Building!​

In this guide, we covered the basics of the new digital asset standard by Nifty OSS. You now have the tools to build a client-side application that can do all sorts of things with digital assets on Solana. So what are you waiting for? Let's get building! If you are ready to bring your project to mainnet, get started with a Solana Mainnet Endpoint for free at QuickNode.com.

If you have a question or idea you want to share, drop us a line on Discord or Twitter!

We ❀️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Resources​

Share this guide