Skip to main content

What is Metaplex Core and How to Mint Your First Core NFT

Updated on
Apr 2, 2024

12 min read

Overview

Metaplex recently announced a new lightweight NFT standard on Solana called Metaplex Core. This guide will teach you about Metaplex Core, why it is important, and how to mint your first NFT using Metaplex Core and the Umi JavaScript library.

Let's jump in!

What You Will Do

Write a script to mint a Metaplex Core NFT using the Umi JavaScript library.

What You Will Need

Dependencies Used in this Guide

DependencyVersion
@metaplex-foundation/umi^0.9.1
@metaplex-foundation/umi-bundle-defaults^0.9.1
@metaplex-foundation/mpl-core^0.2.0
@solana/web3.js^1.91.1
solana cli1.18.1

Let's get started!

What is Metaplex Core?

Metaplex Core is a lightweight Solana NFT standard that leverages a single-account design. This design allows for more efficient execution, lower minting costs, and increased composability. The original Metaplex standard is built on top of Solana's SPL token standard, which requires many accounts to mint an NFT (e.g., token account, metadata account, and master edition accounts). Their associated programs must be invoked to mint an NFT. Not only can this be tricky to manage, it can also be expensive (more than 0.02 SOL per mint compared to .0037) and utilize a lot of compute. The higher compute requirements can mean that transactions may be more likely to fail (due to exceeding the compute limit) and can be limiting when using Cross-Program Invocations (CPIs) to interact with NFTs in your own programs.

Core utilizes a single account structure with optional add-ons. Let's take a look. The Core account struct is defined as follows:

FieldSize (bytes)Description
key1The account discriminator.
owner32The owner of the asset.
update_authority33The update authority of the asset.
name4 + lengthThe name of the asset.
uri4 + lengthThe URI of the asset that points to the off-chain data.
seq1 + (optional, 8)The sequence number used for indexing with compression.

Source: GitHub: MPL Core

In addition to the Core account struct, Core also supports optional add-ons. Plugins can be attached to Core Assets or Collection Assets, allowing plugins to modify the behavior of a single asset or an entire collection. There are three types of plugins:

  • Owner-managed plugins: These plugins are managed by the owner of the asset or collection.
  • Authority-managed plugins: These plugins are managed by the authority of the asset or collection.
  • Permanent: These plugins are permanent and cannot be removed. They must be initialized at the time of creation.

Here's a summary of the Plugins available in Core:

Plug-inTypeAvailable forDescription
Transfer DelegateOwner-managedCore AssetAllows owner to delegate a program that can transfer the asset.
Freeze DelegateOwner-managedCore AssetAllows owner to delegate a program that can freeze the asset.
Burn DelegateOwner-managedCore AssetAllows owner to delegate a program that can burn the asset.
RoyaltiesAuthority-managedCore or CollectionSet royalties and rules for the asset.
Update DelegateAuthority-managedCore or CollectionAllows authority to delegate a program that can update the asset.
AttributeAuthority-managedCore or CollectionStores key-value pairs of data (e.g., traits).
Permanent Transfer DelegatePermanentCore or CollectionAllows owner to delegate a program that can transfer the asset.
Permanent Freeze DelegatePermanentCore or CollectionAllows owner to delegate a program that can freeze the asset.
Permanent Burn DelegatePermanentCore or CollectionAllows owner to delegate a program that can burn the asset.

Source: developers.metaplex.com

Let's try minting a Core NFT!

Create a New Project

mkdir core-demo && cd core-demo && echo > app.ts

Install Solana Web3 dependencies:

yarn init -y
yarn add @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-inscription @solana/web3.js

or

npm init -y
npm install @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-core @solana/web3.js

Set Up Local Environment

Fetch Core Program

You are welcome to use Solana Devnet, but we will use Localnet for this guide for faster transaction times. To use Core on Localnet, we must first deploy the Core program on our local network. To do this, we need to use the solana program dump command. This will effectively fetch the Core program from Solana's devnet and store it so we can deploy it on our local network.

In your terminal, run the following command:

solana program dump -u d CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d core.so

This will download the program executable stored on devnet for the Core program (Program ID: CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d) and save core.so in your current directory. Note where this file is saved, as we will need it later.

Import Dependencies

In your project directory, open app.ts and import the necessary dependencies:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import {
createV1,
mplCore,
fetchAssetV1,
transferV1,
createCollectionV1,
getAssetV1GpaBuilder,
Key,
updateAuthority,
pluginAuthorityPair,
ruleSet
} from '@metaplex-foundation/mpl-core'
import { TransactionBuilderSendAndConfirmOptions, generateSigner, signerIdentity, sol } from '@metaplex-foundation/umi';

We are importing a few methods from the metaplex libraries. We will cover these in more detail as we use them.

Initialize Umi

Next, we will initialize Umi and set up our signer and payer. Add the following code to your app.ts file:

const umi = createUmi('http://127.0.0.1:8899', 'processed').use(mplCore())

const asset = generateSigner(umi);
const payer = generateSigner(umi);

umi.use(signerIdentity(payer));

const txConfig: TransactionBuilderSendAndConfirmOptions = {
send: { skipPreflight: true },
confirm: { commitment: 'processed' },
};

In this code snippet, we are initializing Umi with the localnet endpoint and setting the processed commitment level (to allow for faster processing times in our demonstration). We are also generating a signer for the asset and payer. The payer will be used to pay for the transaction fees (this is set by using umi.use). We also define a txConfig object that we will pass in our transactions to set the skipPreflight and commitment options.

Create NFT Minting Logic

Create a Main Function

Next, we will create a main function that will house the logic of our script. Add the following code to your app.ts file:

async function main() {
// 1. Airdrop to payer
console.log('1. Airdropping to: ', payer.publicKey.toString());
const airdropAmount = sol(100);
await umi.rpc.airdrop(umi.identity.publicKey, airdropAmount, txConfig.confirm);

// 2. Create a collection asset


// 3. Create an asset in a collection


// 4. Fetch assets by owner


// 5. Fetch assets by collection


// 6. Transfer an asset

}

main().catch(console.error);

Here, we have created a function with six steps (that we will populate in the next steps). We have pre-populated the first step, which is to airdrop some SOL to the payer account. This is necessary to pay for the transaction fees.

Create a Collection Asset

Next, we will create a collection asset. Add the following code to section 2 of your main function:

    // 2. Create a collection asset
const collectionAddress = generateSigner(umi);
console.log('2. Creating Collection:', collectionAddress.publicKey.toString());
const collectionUpdateAuthority = generateSigner(umi);
const creator1 = generateSigner(umi);
const creator2 = generateSigner(umi);
await createCollectionV1(umi, {
name: 'Quick Collection', // 👈 Replace this
uri: 'https://your.domain.com/collection.json', // 👈 Replace this
collection: collectionAddress,
updateAuthority: collectionUpdateAuthority.publicKey,
plugins: [
pluginAuthorityPair({
type: 'Royalties',
data: {
basisPoints: 500,
creators: [
{
address: creator1.publicKey,
percentage: 20,
},
{
address: creator2.publicKey,
percentage: 80,
},
],
ruleSet: ruleSet('None'), // Compatibility rule set
},
}),
],
}).sendAndConfirm(umi, txConfig);

Here, we are creating two new signers (collectionUpdateAuthority and collectionAddress), though the authority is optional (default is the payer). We then create a new collection asset using the createCollectionV1 method from the mpl-core library. We are passing in the collection name, URI, collection address, and update authority.

Make sure to update your name and uri with your desired values. You can use QuickNode's IPFS Service to upload and host your NFT image and metadata.

We use a Royalties plugin to set royalties for the collection. For this example, we use two random creators and set the basis points to 500 (5%). A neat feature about Core Assets is that royalties can be set at the collection level and will be applied to all assets in the collection. This is a simple way to manage royalties from a single account (and means less bloat for our NFTs). I encourage you to explore the pluginAuthorityPair method to see other plugin/param pairs that you can add to your collection.

We then send and confirm the transaction using our umi instance and txConfig object.

Create an Asset in a Collection

Now that we have a collection let's mint an asset to it.

    // 3. Create an asset in a collection
await createV1(umi, {
name: 'Quick Asset #1', // 👈 Replace this
uri: 'https://your.domain.com/asset-id.json', // 👈 Replace this
asset: asset,
collection: collectionAddress.publicKey,
authority: collectionUpdateAuthority,
}).sendAndConfirm(umi, txConfig);

This time, we are using the createV1 method to create a new asset. We are passing in the asset name, URI (make sure to update with your own), asset address, collection address and collection's update authority. A couple of things to note here:

  • We do not need to set royalties for the asset, as they are inherited from the collection.
  • To mint an asset to a collection, we must pass the collection's update authority as a Signer object. This is because the collection's update authority is required to update the collection's assets.

Fetch Assets by Owner

Currently, Core is not supported by the Digtal Asset Standard, so we will need to use getProgramAccounts to fetch the assets. Metaplex includes a helper function for building a GPA query, getAssetV1GpaBuilder. Add the following code to sections 4 and 5 of your main function:

    // 4. Fetch assets by owner
const assetsByOwner = await getAssetV1GpaBuilder(umi)
.whereField('key', Key.AssetV1)
.whereField('owner', payer.publicKey)
.getDeserialized();

console.log(assetsByOwner);

// 5. Fetch assets by collection
const assetsByCollection = await getAssetV1GpaBuilder(umi)
.whereField('key', Key.AssetV1)
.whereField(
'updateAuthority',
updateAuthority('Collection', [collectionAddress.publicKey])
)
.getDeserialized();

console.log(assetsByCollection);

Both queries are very similar--they pass our umi instance into the getAssetV1GpaBuilder method and set the key field to Key.AssetV1 (imported from mpl-core). The first query fetches assets by owner, and the second query fetches assets by collection. The second query also sets the updateAuthority field to updateAuthority('Collection', [collectionAddress.publicKey]) to filter by the collection address.

Each should log an array of assets to the console.

Transfer an Asset

Finally, let's transfer an asset to another account. Add the following code to section 6 of your main function:

    // 6. Transfer an asset
const recipient = generateSigner(umi);
await transferV1(umi, {
asset: asset.publicKey,
newOwner: recipient.publicKey,
collection: collectionAddress.publicKey,
}).sendAndConfirm(umi, txConfig);

If you have ever done any transfers with SPL tokens, this should look excitingly simple! This is one of the significant benefits of a single account model--no worrying about token accounts or token account initialization. Simply pass the new destination, and make sure you have a valid authority to transfer the asset 🙌. Note that for assets in collections, we will need to pass the collection address.

You can verify the asset has been transferred by adding the following:

    const transferredAsset = await fetchAssetV1(umi, asset.publicKey);
if (transferredAsset.owner.toString() !== recipient.publicKey.toString()) {
throw new Error('Transfer failed');
}

Great job! Let's test our script.

Run the Script

To run our code on localnet, we will need two terminal windows open. In the first terminal, start a localnet with your Core program deployed. Run:

solana-test-validator -r --bpf-program CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d core.so

Make sure to replace core.so with the correct path to the file you downloaded earlier using solana program dump.

After the localnet is running, in the second terminal, run your script:

ts-node app.ts

You should see the output of each step in your terminal! Great job!

If you run into any errors, double-check your code and ensure you have the correct dependencies installed. If you are still having trouble, feel free to reach out to us on Discord or check out the QuickNode Forum.

Continued Practice

Want to keep practicing with Core? Here are a few ideas to get you started:

  • Try out the addPluginV1 and createPlugin methods to add a FreezeDelegate to your NFT to enable escrowless staking.
  • Then try using revokePluginAuthorityV1 to remove the FreezeDelegate from your NFT.
  • Burn an NFT using the burnV1 method.
  • Update an NFT using the updateV1 method or a collection using the updateCollectionV1 method.

Ready to Launch Your Project?

If you are ready to move off localhost, QuickNode has the tools to deploy your project quickly. Get started with a free developer plan today!

Wrap Up

Metaplex Core creates exciting new opportunities for building lower-cost Solana NFT projects with higher composability. You can now mint, transfer, and manage NFTs with a single account. We are excited to see what you're building. Drop us a line on Discord and let us know what you're working on!

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