Metaplex + Open Creator Protocol

Levan Ilashvili
9 min readJan 4, 2023

--

On December 1, 2022 MagicEden announced new creator royalty enforcement tool, Open Creator Protocol, which had some impressive things to offer to the creators. Here are main insights from their blog post:

- Effective immediately, new collections can launch with our open-source Open Creator Protocol to protect royalties
- The tool is entirely open source and built to serve creators. It is a free option for the community to try.
- Magic Eden will protect royalties on all collections who adopt the protocol. The protocol allows creators to ban marketplaces that have not protected royalties on their collection
- Additional open Creator Protocol features include freeze authority, dynamic royalties and customizable token transferability
- Existing collections will have the option to burn and re-mint their existing collections on the Open Creator Protocol

At the same time, Metaplex announced MIP-1 — Metaplex standard which will enforce royalties, but that’s scheduled for Q1 2023 — lots of royalties will be lost until creators migrate to MIP-1 (that’s a long shot — we assume it’s out in Q1 2023 and Metaplex offers easy path for migration), so at the moment only royalty-enforcement tool is OCP.

New protocols, standards, tools, etc. are hard to understand when there are no samples or docs. It is the case with OCP — it’s a great tool, but it’s not easy to grasp at first sight. That’s why I decided to dive in and write a small article, demonstrating usage of OCP along with Metaplex.

Time to roll up our sleeves and get to work — today we will mint NFT on Solana and make sure our royalties stay protected on MagicEden (and other marketplaces, when they decide to support this standard)

Before we dive into code, let’s remember some basic things:

Token Metadata Program

Token Metadata program is responsible for attaching additional data to tokens on Solana — it’s one of the most important programs in the ecosystem, both for fungible and non-fungible tokens.

If you haven’t heard of this — I strongly recommend pausing here and reading docs — it’s very well documented and easy to undestand

Metaplex Certified Collections

There was a time when collections on Solana chain were defined by collection property in external metadata. That was a problem — and it got resolved. Starting from v1.3, Token Metadata program supports sized collections, burning NFTs, adding and removing NFTs to collections. Here are more details: Metaplex Certified Collections

and let’s also go through important packages we’ll need to mint NFTs:

  • @metaplex-foundation/js — SDK which will get us started with all on-chain tools provided by Metaplex
  • @project-serum/anchor — Typescript client for Anchor programs — that’s my choice for working with Solana chain
  • @metaplex-foundation/mpl-token-metadata — token vault contract SDK code. You can look at docs here

also, we will need Solana wallet (you can create one with Anchor, Solana CLI, Phantom, etc.) and some SOL. If you are using devnet, you can get some SOL with airdrop 😉

I will go with mainnet implementation for this article and mint my avatar as NFT 😃

Step 1: Let’s generate wallet and save keypair in article.json file. Code is simple:

import * as anchor from '@project-serum/anchor';
import { writeFileSync } from 'fs';

function generateWallet() {
const wallet = anchor.web3.Keypair.generate();
console.log(wallet.publicKey);
writeFileSync(`article.json`, JSON.stringify(wallet));
}

generateWallet();

I named this file article.ts , so if I run this using

npx ts-node article.ts

I get tutorial.json file with my keypair (public and secret keys) and also, I see my public key in console. I got this public key: 2xsAsaEhXVwKU1oSXBjeJqafeRLh6Yt58zziTYNrLXMn and next thing I’m gonna do is send some SOL to it. Here’s funding tx — 0.1 SOL should be enough for rest of this article.

Next thing we want to do is set up connection with Solana mainnet(-beta) and load our wallet. Also, we don’t need that generateWallet() method anymore, so let’s move it somewhere else or remove it (you can just have it in separate file in your useful scripts folder).

import * as anchor from '@project-serum/anchor';
import { Connection } from '@solana/web3.js';
import keyPair from './article.json';

const walletKeyPair =
anchor.web3.Keypair.fromSecretKey(
new Uint8Array(
Object.values(keyPair._keypair.secretKey)
)
);

const wallet = new anchor.Wallet(walletKeyPair);

const connection = new anchor.AnchorProvider(
new Connection("https://api.mainnet-beta.solana.com"),
new anchor.Wallet(wallet),
{ commitment: "confirmed" }
).connection;

What we are doing here is this: we load our article.json, grab secret key numbers, build Uint8Array with them and create wallet instance with it. And with connection — we’re just setting up new connection with mainnet-beta cluster. In production I recommend using QuickNodes or some other provider — you’ll get better rate limits.

Okay, now we have connection. Next step is minting our NFT.

As I mentioned in the beginning of this article, NFTs on Solana are heavily relying on Token Metadata program — this guy allows us to attach additional metadata to our tokens. Solana NFTs have two ‘sets’ of metadata — on-chain metadata, which — as name suggests — is stored on-chain, and off-chain metadata, which is stored on some decentralized storage or your server or S3 bucket, etc. Here is a nice schema from Metaplex docs, describing how data is stored:

Let’s construct our on-chain metadata first:

const onChainMetadata = {
name: `Toby on Solana`,
symbol: "TOBY",
uri: "https://raw.githubusercontent.com/LevanIlashvili/articles/main/ocp/metadata.json",
sellerFeeBasisPoints: 500,
creators: [
{
address: wallet.publicKey,
verified: false,
share: 100
},
],
collection: null,
uses: null,
} as DataV2;

Since article of this goal is not discussing file storage, I’m just gonna use GitHub to store my off-chain metadata.json and image 😃 Let’s make sure that we have proper JSON file on Github:

{
"attributes":[
{
"trait_type":"Name",
"value":"Toby"
},
{
"trait_type":"Breed",
"value":"Rottweiler"
},
{
"trait_type":"Birthday",
"value":"February 8, 2019"
},
{
"trait_type":"Personality",
"value":"Good Boi"
}
],
"description":"Toby is a good boi",
"external_url":"https://levan.blog/",
"name":"Toby the Rottweiler",
"image":"https://raw.githubusercontent.com/LevanIlashvili/articles/main/ocp/toby.jpeg",
"properties":{
"category":"image",
"creators":[
{
"address":"2xsAsaEhXVwKU1oSXBjeJqafeRLh6Yt58zziTYNrLXMn",
"share":100
}
],
"files":[
{
"uri":"https://raw.githubusercontent.com/LevanIlashvili/articles/main/ocp/toby.jpeg",
"type":"image/png"
}
]
},
"seller_fee_basis_points":500,
"symbol":"TOBY"
}

and of course, I uploaded toby.jpeg on GitHub as well.

Next thing we need is mint keypair — mint account is the account we’re actually minting our NFT to. Let’s use that script we wrote in the beginning of this article to generate one more account (in most of the cases, we don’t actually need to store mint keypair at all, but since this is an article and I don’t know what goes wrong — let’s keep the keypair of our mint account. I generated mint account and saved keypair to file article-mint.json . We can load it the same way we load our wallet:

import mintKey from './article-mint.json';

const mintKeyPair = anchor.web3.Keypair.fromSecretKey(
new Uint8Array(
Object.values(mintKey._keypair.secretKey)
)
);

Now we get to the most interesting / fun part — let’s build transaction and mint Toby (actually — it’s my Toby, you should mint your own doggo)

Here’s order of operations we need to perform:

  • Get minimum balance required to make account rent exempt
  • Get Metadata PDA for our mint public key
  • Build a transaction with following instructions: create new account with mint public key, initialise mint instruction, create metadata
  • We will need to sign this transaction with both our wallet and mint account’s keys.
  • Broadcast transaction

Let’s translate those instructions into TypeScript:

// Get minimum balance required to make account rent exempt
const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);

// Get Metadata PDA for our mint public key
const metadataPDA = findMetadataPda(mintKeyPair.publicKey);

and for transaction, we’ll need three instructions:

// Build transaction 
const tx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: walletKeyPair.publicKey,
newAccountPubkey: mintKeyPair.publicKey,
space: MINT_SIZE,
lamports: requiredBalance,
programId: TOKEN_PROGRAM_ID,
}),
createInitializeMintInstruction(
mintKeyPair.publicKey, //Mint Address
0, //Number of Decimals of New mint
walletKeyPair.publicKey, //Mint Authority
walletKeyPair.publicKey, //Freeze Authority
TOKEN_PROGRAM_ID
),
createCreateMetadataAccountV2Instruction(
{
metadata: metadataPDA,
mint: mintKeyPair.publicKey,
mintAuthority: walletKeyPair.publicKey,
payer: walletKeyPair.publicKey,
updateAuthority: walletKeyPair.publicKey,
},
{
createMetadataAccountArgsV2: {
data: onChainMetadata,
isMutable: true,
},
}
)
);

Before signing and broadcasting transaction, we need to set feePayer and recentBlockHash. Fee payer can be our wallet, recent block hash — we can grab from chain:

const recentBlockhash = await connection.getLatestBlockhash('finalized')
tx.recentBlockhash = recentBlockhash.blockhash;
tx.feePayer = wallet.publicKey;

Let’s put everything in one function, called createAccounts() and see what it does — we have this code:

import { createCreateMetadataAccountV2Instruction, DataV2 } from '@metaplex-foundation/mpl-token-metadata';
import * as anchor from '@project-serum/anchor';
import { Connection, sendAndConfirmRawTransaction, SystemProgram, Transaction } from '@solana/web3.js';
import { createInitializeMintInstruction, getAssociatedTokenAddress, getMinimumBalanceForRentExemptMint, MINT_SIZE, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { findMetadataPda } from '@metaplex-foundation/js';

import keyPair from './article.json';
import mintKey from './article-mint.json';
const walletKeyPair = anchor.web3.Keypair.fromSecretKey(
new Uint8Array(
Object.values(keyPair._keypair.secretKey)
)
);
const wallet = new anchor.Wallet(walletKeyPair);
const mintKeyPair = anchor.web3.Keypair.fromSecretKey(
new Uint8Array(
Object.values(mintKey._keypair.secretKey)
)
);

const connection = new anchor.AnchorProvider(
new Connection("https://api.mainnet-beta.solana.com"),
new anchor.Wallet(walletKeyPair),
{ commitment: "confirmed" }
).connection;
async function createAccounts() {
const onChainMetadata = {
name: `Toby on Solana`,
symbol: "TOBY",
uri: "https://raw.githubusercontent.com/LevanIlashvili/articles/main/ocp/metadata.json",
sellerFeeBasisPoints: 500,
creators: [
{
address: wallet.publicKey,
verified: false,
share: 100
},
],
collection: null,
uses: null,
} as DataV2;

// Get minimum balance required to make account rent exempt
const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);

// Get Metadata PDA for our mint public key
const metadataPDA = findMetadataPda(mintKeyPair.publicKey);

// Build transaction
const tx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: walletKeyPair.publicKey,
newAccountPubkey: mintKeyPair.publicKey,
space: MINT_SIZE,
lamports: requiredBalance,
programId: TOKEN_PROGRAM_ID,
}),
createInitializeMintInstruction(
mintKeyPair.publicKey, //Mint Address
0, //Number of Decimals of New mint
walletKeyPair.publicKey, //Mint Authority
walletKeyPair.publicKey, //Freeze Authority
TOKEN_PROGRAM_ID
),
createCreateMetadataAccountV2Instruction(
{
metadata: metadataPDA,
mint: mintKeyPair.publicKey,
mintAuthority: walletKeyPair.publicKey,
payer: walletKeyPair.publicKey,
updateAuthority: walletKeyPair.publicKey,
},
{
createMetadataAccountArgsV2: {
data: onChainMetadata,
isMutable: true,
},
}
)
);

const recentBlockhash = await connection.getLatestBlockhash('finalized')
tx.recentBlockhash = recentBlockhash.blockhash;
tx.feePayer = wallet.publicKey;

tx.partialSign(mintKeyPair);
await wallet.signTransaction(tx);
const sig = await sendAndConfirmRawTransaction(connection, tx.serialize());
console.log(sig);
}
createAccounts();

Take a deep breath, run: npx ts-node article.ts and after few seconds, you’ll get magic string in the console. I got this transaction: 2MFLjKPrJJFZhYdQCz1m79C6KJCyXodsFXua2JihBNczZdmsn31NzUyejdMmpJ1SAGy4FhZn3kCLhE1NSUmUWgem

It’s important to know that we haven’t minted anything yet — we created accounts. We could mint NFT as well, but remember what’s our goal? Yeap, it’s using Open Creator Protocol — so we should actually let OCP program mint our NFT

For this part we’ll need to use instructions of OCP program

Simplest way of getting generated files for Typescript is cloning OCP repository and moving files from sdk/src to your project’s folder. We will use following imports from OCP:

import { 
LARGER_COMPUTE_UNIT,
createWrapInstruction,
findMintStatePk,
CMT_PROGRAM,
createInitAccountInstruction,
findFreezeAuthorityPk,
createMintToInstruction as ocpCreateMintToInstruction
} from "./src";

Another important thing to keep in mind is OCP Policy — that’s what tells OCP program about permissions you’re setting for your NFTs. You can read more about it here (and learn how to create/update your own policy). For now, we will use on-chain accounts that were created by OCP creators — you can find about it here. More specifically, we’ll use ALLOW_ALL policy, and it’s corresponding test account.

Here’s what we need to do next, to mint NFTs — again, let’s make a list in plain English and then translate it to TypeScript:

  • Pick on-chain policy
  • Get Associated Token Address for our mint public key and our wallet’s public key
  • Set compute unit limit for transaction
  • Create Wrap instruction
  • Create Init Account instruction
  • Create OCP’s mint instruction
  • Set fee payer, recent block hash, sign and broadcast

and then sign transaction and broadcast.

I will put all these in another function, wrapAndMint()

async function wrapAndMint() {
const policy = new PublicKey("7evQhBswiztNd6HLvNWsh1Ekc3fmyvQGnL82uDepSMbw");

// Get Associated Token Address for our mint public key and our wallet's public key
const associatedTokenAddress = await getAssociatedTokenAddress(
mintKeyPair.publicKey,
walletKeyPair.publicKey
);

const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: LARGER_COMPUTE_UNIT }),
createWrapInstruction({
mint: mintKeyPair.publicKey,
policy,
freezeAuthority: wallet.publicKey,
mintAuthority: wallet.publicKey,
mintState: findMintStatePk(mintKeyPair.publicKey),
from: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
cmtProgram: CMT_PROGRAM,
metadata: findMetadataPda(mintKeyPair.publicKey),
}),
createInitAccountInstruction({
policy,
freezeAuthority: findFreezeAuthorityPk(policy),
mint: mintKeyPair.publicKey,
metadata: findMetadataPda(mintKeyPair.publicKey),
mintState: findMintStatePk(mintKeyPair.publicKey),
from: wallet.publicKey,
fromAccount: associatedTokenAddress,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
cmtProgram: CMT_PROGRAM,
payer: wallet.publicKey,
}),
ocpCreateMintToInstruction({
policy,
freezeAuthority: findFreezeAuthorityPk(policy),
mint: mintKeyPair.publicKey,
metadata: findMetadataPda(mintKeyPair.publicKey),
mintState: findMintStatePk(mintKeyPair.publicKey),
from: wallet.publicKey,
fromAccount: associatedTokenAddress,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
cmtProgram: CMT_PROGRAM,
payer: wallet.publicKey,
})
);
const recentBlockhash = await connection.getLatestBlockhash('finalized')
tx.recentBlockhash = recentBlockhash.blockhash;
tx.feePayer = wallet.publicKey;
await wallet.signTransaction(tx);
const sig = await sendAndConfirmRawTransaction(connection, tx.serialize());
console.log(sig);
}

wrapAndMint();

All we have to do is run it and wait for the magic string in the console. After few seconds I got 4i2c8nGvVM9YtswAEgTh9oVZ8Es4DR1v61zRsaNL7LKSqnNsUd5TbkGR8D9RzGLACQ66bK9e2fiXxngvdMzPFqA9

As you can see, my NFT got actually minted:

and here is NFT

And this is it — we minted NFT using Metaplex and wrapped it with Open Creator Protocol. We skipped few steps, like — setting on-chain collection and verifying creator/collection — but that’s beyond the scope of this article.

Thanks for your time and yeah.. mint some fun collections and stop losing royalty fees 😉

--

--

Levan Ilashvili
Levan Ilashvili

Written by Levan Ilashvili

Hooman of two doggos, software engineer

No responses yet