All you need to know about the NEW Oracle Plugin

LE◎
Metaplex
8 min readMay 30, 2024

--

With the development of the Metaplex Core standard for digital assets, our team has been able to move faster than ever in creating new functionalities.

In the newest version of Metaplex Core we’re excited to introduce the first External Plugin for all Core assets. Today, we’ll dive into the details of these plugins and the Oracle Plugin in particular!

This article will cover everything you need to know about the Oracle Plugin and give some context on External Plugins, and Core. We’ll explore possible use cases, explain how the plugin works, and wrap it up with a code example in Rust and Typescript.

PS: This is for non-technical people too! So stay until the end to learn everything you need to know to implement it in your project! And if you’re still having problems, reach out to the community developers on Discord!

Setting the Stage: What are Core External Plugins?

Before diving deep into the Oracle Plugin Adapter, it makes sense to set the stage with some context on how this revolutionary new standard works.

Metaplex Core

Metaplex Core (“Core”) is a new standard created specifically for digital assets. By moving away from the standard SPL-Token Program, Metaplex eliminated the complexity and technical debt of the previous standard (Token Metadata), providing a clean and simple interface for digital assets.

This new implementation uses a single account design, significantly reducing minting costs and optimizing network load by streamlining instruction processes. Additionally, it features a flexible plugin system that allows developers to modify the behavior and functionality of assets.

Here are the biggest differences and improvements:

  • Unprecedented Cost Efficiency: Metaplex Core offers the lowest minting costs compared to available alternatives. For instance, an NFT that would cost 0.022 SOL with Token Metadata or 0.0046 SOL with Token Extensions can be minted with Core for just 0.0029 SOL.
  • Low Compute: Core operations have a small Compute Unit footprint. This allows more transactions to be included in one block. Instead of 205,000 CU for minting, Core requires just 17,000 CU.
  • Single Account Design: Unlike relying on a fungible Token Standard like SPL Token or Token Extensions (aka Token22), Core focuses on the needs of an NFT standard. This allows Core to use a single account that also tracks the owner.

External Plugin

A plugin is like an on-chain app for your NFT that can either store data or provide additional functionality to the asset.

External Plugins are part of the Authority-managed Plugin family, meaning only the authority of the Core Asset or Core Collection can add and update the plugin on the asset.

Note: If an Authority Managed Plugin is added to an asset/collection without an authority argument present, the plugin will default to the authority type of update authority.

There are two different parts in the external plugin family:

  1. Adapter: Allows data and validations to be passed from an External Plugin.
  2. External Plugin: Provides the data and validation for the Adapter.

Each External Adapter has the ability to assign lifecycle checks to Lifecycle Events, influencing the behavior of the lifecycle event taking place.

This means we can assign the following checks to lifecycle events like create, transfer, update and burn:

  • Listen: “web3” webhook that alerts the plugin that a lifecycle event has occurred. This is particularly useful for tracking data or performing actions.
  • Reject: The plugin has the ability to reject a lifecycle event.
  • Approve: The plugin has the ability to approve a lifecycle event.

These plugins are called External because the plugin’s behavior is controlled by an “external” source. The core program provides an adapter, but developers decide the behavior by pointing this adapter to an external source of data.

The cool thing about it is that thanks to the structure of the data stored in the external plugin, the Metaplex DAS (Digital Asset Standard) API can index this data without any additional integration.

Note: The Data Authority of an External Plugin is the only authority allowed to write to the External Plugin’s data section.

Starting off — What is the Oracle Plugin Adapter and why should you care about it?

Now that we have a better understanding of how core assets and External Plugins work, let’s dive into the new Oracle Plugin Adapter.

What is an Oracle plugin?

An Oracle Plugin leverages the capability of external plugins to save data that an external authority can update.

Specifically, the Oracle Plugin Adapter can access on-chain data accounts external to the Core asset and based on that, assets can dynamically reject lifecycle events set by the asset authority. The external Oracle account can also be updated at any time to change the authorization behavior of the lifecycle events, providing a flexible and dynamic experience.

Note: differently from the other external plugins, the Oracle plugin adapter can just reject lifecycle hooks and doesn’t have a data section.

Even though this use case requires on-chain implementation likely handled in Native Rust or Anchor, it significantly lowers the entry barrier for many web2 developers who want to explore digital assets in their flow.

Note: these developers primarily work within a web2 framework but are starting to integrate web3 elements like NFTs into their websites and apps.

As adoption grows, we can expect more plug-and-play Oracle plugins maintained and created by third-party users, and possibly entire protocols designed to keep Core-Oracle data dynamic and current. This will allow anyone to point to a specific address to obtain their desired functionality, making the integration process straightforward and efficient for web2 developers.

How does it work?

The Oracle Plugin stores data relating to four lifecycle events: create, transfer, burn, and update. It can be configured to perform only a Reject validation instead of an approve validation or listen hook like the other plugins.

Other than that, the Oracle Plugin can be integrated at either the single asset level or for an entire collection

As we highlighted before to implement this, we need an on-chain validation account structured as follows:

#[account]
pub struct Validation {
pub validation: OracleValidation,
}

pub enum OracleValidation {
Uninitialized,
V1 {
create: ExternalValidationResult,
transfer: ExternalValidationResult,
burn: ExternalValidationResult,
update: ExternalValidationResult,
},
}

pub enum ExternalValidationResult {
Approved,
Rejected,
Pass,
}

The validation account structure can be customized but must have the OracleValidation struct.

Other than this developers need to make sure to:

  • Reserve enough space for the OracleValidation enum (5 bytes).
  • Pay attention to where the OracleValidation enum is placed. The account structure may vary slightly based on different frameworks and needs. Metaplex allows for NoOffset, AnchorOffset, or CustomOffset options for the validation struct.

Additionally, the plugin is designed flexibly so that it does not require a fixed address to access Oracle functions. Instead, it can use a dynamic address based on protocol-defined seeds.

Examples of how protocols could integrate the Oracle Plugin Adapter into their NFTs

  • Create a Proof-of-Work NFT: Enable minting of the NFT every time someone solves a cryptographic hash. The Oracle data can be updated to approve the creation of new NFTs and then reverted once the NFT is minted.
  • TradFi Market for NFT: Allow trading or transferring of NFTs only during stock market trading hours.
  • A Pokemon-Style Rewarding Pass: Implement a system where users gain experience points on their pass and can evolve their “Pokemon” only after completely filling the experience bar for the level.
  • In-Game Curses for NFT Gaming: Develop a mechanism where in-game curses are non-burnable and soul-bound until the player completes a specific ritual.

Let’s get our hands dirty — Creating an NFT with the Oracle Plugin.

Now that you know everything about Oracle NFTs, let’s create an example using them!

On-chain Oracle Account

First, we need to create an on-chain Oracle Account. In this guide, we’ll use the Anchor framework, but you can also implement it using a native program. Learn more about the Anchor framework here: Anchor Framework.

Note: This example is the “hello world” of Oracle Accounts. You can follow along and open the example in Solana Playground, an online tool to build and deploy Solana programs: Solana Playground.

Let’s start by creating our state.rs file, where we'll store the Validation Data that needs updating:

use anchor_lang::prelude::*;
use mpl_core::types::OracleValidation;

#[account]
pub struct Validation {
pub validation: OracleValidation,
}

impl Validation {
pub fn size() -> usize {
8 // anchor discriminator
+ 5 // validation
}
}

Next, we’ll need an instruction to initialize the account that contains the OracleValidation. We can easily do this by creating an instruction like this:

use mpl_core::types::{OracleValidation, ExternalValidationResult};

pub fn oracle_init(ctx: Context<OracleAccountInit>) -> Result<()> {
// Initialize and set fixed account data.
ctx.accounts.oracle_account.validation = OracleValidation::V1 {
create: ExternalValidationResult::Pass,
transfer: ExternalValidationResult::Pass,
burn: ExternalValidationResult::Pass,
update: ExternalValidationResult::Rejected,
};

Ok(())
}

#[derive(Accounts)]
pub struct OracleAccountInit<'info> {
pub signer: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(init, payer = payer, space = Validation::size())]
pub oracle_account: Account<'info, Validation>,
pub system_program: Program<'info, System>,
}

Now we need a way to update the Oracle data when needed. This is straightforward and similar to what we did when initializing the account:

use mpl_core::types::{OracleValidation, ExternalValidationResult};

pub fn oracle_update(ctx: Context<OracleUpdate>) -> Result<()> {
// Initialize and set fixed account data.
ctx.accounts.oracle_account.validation = OracleValidation::V1 {
create: ExternalValidationResult::Pass,
transfer: ExternalValidationResult::Pass,
burn: ExternalValidationResult::Pass,
// We changed the update lifecycle to pass!
update: ExternalValidationResult::Pass,
};

Ok(())
}

#[derive(Accounts)]
pub struct OracleUpdate<'info> {
pub signer: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub oracle_account: Account<'info, Validation>,
pub system_program: Program<'info, System>,
}

Create the NFT

Now let’s create our asset collection and point it to our Oracle account so every asset we include in that collection will follow our custom Oracle rule!

Start by setting up your environment to use Umi. (Umi is a modular framework for building and using JavaScript clients for Solana programs. Learn more here: Umi Framework)

import { createSignerFromKeypair, signerIdentity } from '@metaplex-foundation/umi'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'

// SecretKey for the wallet you're going to use
import wallet from "../wallet.json";

const umi = createUmi("https://api.devnet.solana.com", "finalized")

let keyair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(wallet));
const myKeypairSigner = createSignerFromKeypair(umi, keyair);
umi.use(signerIdentity(myKeypairSigner));

Next, we create the collection including the Oracle Plugin using CreateCollection:

// Generate the Collection PublicKey
const collection = generateSigner(umi)
console.log("Collection Address: \n", collection.publicKey.toString())

const oracleAccount = publicKey("...")

// Generate the collection
const collectinTx = await createCollection(umi, {
collection: collection,
name: 'My Collection',
uri: 'https://example.com/my-collection.json',
plugins: [
{
type: "Oracle",
resultsOffset: {
type: 'Anchor',
},
baseAddress: oracleAccount,
authority: {
type: 'UpdateAuthority',
},
lifecycleChecks: {
update: [CheckResult.CAN_REJECT],
},
baseAddressConfig: undefined,
}
]
}).sendAndConfirm(umi)

// Deserialize the Signature from the Transaction
let signauture = base58.deserialize(collectinTx.signature)[0];
console.log(signauture);

After creating the collection, you can generate an asset that is assigned directly to the collection using the create instruction:

// Generate the Asset PublicKey
const asset = generateSigner(umi)
console.log(asset.publicKey.toString())

// Generate the Asset
const assetTx = await create(umi, {
name: 'My NFT',
uri: 'https://example.com/my-nft.json',
asset: asset,
collection: collection.publicKey,
}).sendAndConfirm(umi);

// Deserialize the Signature from the Transaction
signauture = base58.deserialize(assetTx.signature)[0];
console.log(signauture);

Aaaaand we’re done! Enjoy your first asset leveraging the new Oracle Plugin!

Congratulations! You are an Oracle expert! I hope this tutorial has been helpful and that you learned something insightful!

Want to learn more about Core, and Metaplex in general? Check out the developer portal: here.

If you want to follow my journey and you don’t want to miss out on my next tutorial, follow me on Twitter!

--

--