浏览代码

Soulbound Assets in MPL-Core (#274)

* initial

* use totem

* add plugin snippet

* show oracle example
MarkSackerberg 11 月之前
父节点
当前提交
f558d4d2a6

+ 6 - 0
src/components/products/core/index.js

@@ -231,6 +231,12 @@ export const core = {
               title: 'Immutability', 
               href: '/core/guides/immutability' 
             },
+            { 
+              title: 'Soulbound Assets', 
+              href: '/core/guides/create-soulbound-nft-asset',
+              created: '2024-12-06',
+              updated: null, // null means it's never been updated
+            },
             { 
               title: 'Print Editions', 
               href: '/core/guides/print-editions'

+ 3 - 26
src/pages/core/faq.md

@@ -19,32 +19,9 @@ Core currently charges a very small fee of 0.0015 SOL per Asset mint to the call
 
 ## How to create a Soulbound Asset?
 
-The Core Standard allows you to create Soulbound Assets. To achieve this use the [Permanent Freeze Delegate](/core/plugins/permanent-freeze-delegate) plugin. On Asset creation you would include the `Permanent Freeze` plugin set to frozen, and with the authority set to none, making the plugins data immutable.
-
-{% dialect-switcher title="Create a Soulbound asset" %}
-{% dialect title="JavaScript" id="js" %}
-
-```ts
-import {
-  createAsset,
-  pluginAuthorityPair,
-  nonePluginAuthority,
-} from '@metaplex-foundation/mpl-core'
-
-await createAsset(umi, {
-  owner,
-  plugins: [
-    pluginAuthorityPair({
-      type: 'PermanentFreeze',
-      data: { frozen: true },
-      authority: nonePluginAuthority(),
-    }),
-  ],
-})
-```
-
-{% /dialect %}
-{% /dialect-switcher %}
+The Core Standard allows you to create Soulbound Assets. To achieve this either the [Permanent Freeze Delegate](/core/plugins/permanent-freeze-delegate) plugin or the [Oracle Plugin](/core/external-plugins/oracle) can be used. 
+
+To learn more check out the [Soulbound Assets Guide](/core/guides/create-soulbound-nft-asset)!
 
 ## How to set an Asset to be Immutable?
 

+ 541 - 0
src/pages/core/guides/create-soulbound-nft-asset.md

@@ -0,0 +1,541 @@
+---
+title: Soulbound Assets in MPL Core
+metaTitle: Soulbound Assets in MPL Core | Core Guides
+description: This Guide explores the different options for soulbound Assets in MPL Core
+---
+
+
+Soulbound NFTs are non-fungible tokens that are permanently bound to a specific wallet address and cannot be transferred to another owner. They are useful for representing achievements, credentials, or memberships that should remain tied to a specific identity.  {% .lead %}
+
+## Overview
+
+In this guide, we'll explore how to create soulbound assets using MPL Core and the Umi Framework. Whether you're a developer looking to implement soulbound NFTs in TypeScript or just want to understand how they work, we'll cover everything from basic concepts to practical implementation. We'll examine different approaches for making assets soulbound and walk through creating your first soulbound NFT within a collection.
+
+In MPL Core, there are two main approaches to create soulbound NFTs:
+
+### 1. Permanent Freeze Delegate Plugin
+- Makes assets completely non-transferrable and non-burnable
+- Can be applied at either:
+  - Individual asset level
+  - Collection level (more rent efficient)
+- Collection-level implementation allows thawing all assets in a single transaction
+
+### 2. Oracle Plugin
+- Makes assets non-transferrable but still burnable
+- Can also be applied at:
+  - Individual asset level  
+  - Collection level (more rent efficient)
+- Collection-level implementation allows thawing all assets in a single transaction
+
+## Creating Soulbound NFTs with the Permanent Freeze Delegate Plugin
+
+The Permanent Freeze Delegate Plugin provides functionality to make assets non-transferrable by freezing them. When creating a soulbound asset, you would:
+
+1. Include the Permanent Freeze plugin during asset creation
+2. Set the initial state to frozen
+3. Set the authority to None, making the frozen state permanent and immutable
+
+This effectively creates a permanently soulbound asset that cannot be transferred or thawed. In the following code snippet it is shown where to add those three options:
+
+```js
+  await create(umi, {
+    asset: assetSigner,
+    collection: collection,
+    name: "My Frozen Asset",
+    uri: "https://example.com/my-asset.json",
+    plugins: [
+      {
+        type: 'PermanentFreezeDelegate', // Include the Permanent Freeze plugin
+        frozen: true, // Set the initial state to frozen
+        authority: { type: "None" }, // Set the authority to None
+      },
+    ],
+  })
+```
+
+
+### Asset-Level Implementation
+The Permanent Freeze Delegate Plugin can be attached to individual assets to make them soulbound. This provides more granular control but requires more rent and separate thaw transactions per asset in case it ever should not be soulbound anymore.
+
+{% totem %}
+{% totem-accordion title="Code Example" %}
+```js
+import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
+import { mplCore } from "@metaplex-foundation/mpl-core";
+import {
+  generateSigner,
+  keypairIdentity,
+  publicKey,
+  sol,
+} from "@metaplex-foundation/umi";
+import {
+  createCollection,
+  create,
+  fetchCollection,
+  transfer,
+  fetchAssetV1,
+} from "@metaplex-foundation/mpl-core";
+import { base58 } from "@metaplex-foundation/umi/serializers";
+
+// Define a dummy destination wallet for testing transfer restrictions
+const DESTINATION_WALLET = publicKey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d");
+
+(async () => {
+  // Step 1: Initialize Umi with devnet RPC endpoint
+  const umi = createUmi(
+    "YOUR ENDPOINT"
+  ).use(mplCore());
+
+  // Step 2: Create and fund a test wallet
+  const walletSigner = generateSigner(umi);
+  umi.use(keypairIdentity(walletSigner));
+
+  console.log("Funding test wallet with devnet SOL...");
+  await umi.rpc.airdrop(walletSigner.publicKey, sol(0.1));
+
+  // Step 3: Create a new collection to hold our frozen asset
+  console.log("Creating parent collection...");
+  const collectionSigner = generateSigner(umi);
+  await createCollection(umi, {
+    collection: collectionSigner,
+    name: "My Collection",
+    uri: "https://example.com/my-collection.json",
+  }).sendAndConfirm(umi);
+  
+  // Wait for transaction confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the collection was created
+  const collection = await fetchCollection(umi, collectionSigner.publicKey);
+  console.log("Collection created successfully:", collectionSigner.publicKey);
+
+  // Step 4: Create a frozen asset within the collection
+  console.log("Creating frozen asset...");
+  const assetSigner = generateSigner(umi);
+  
+  // Create the asset with permanent freeze using the PermanentFreezeDelegate plugin
+  await create(umi, {
+    asset: assetSigner,
+    collection: collection,
+    name: "My Frozen Asset",
+    uri: "https://example.com/my-asset.json",
+    plugins: [
+      {
+        // The PermanentFreezeDelegate plugin permanently freezes the asset
+        type: 'PermanentFreezeDelegate',
+        frozen: true, // Set the asset as frozen
+        authority: { type: "None" }, // No authority can unfreeze it
+      },
+    ],
+  }).sendAndConfirm(umi);
+  
+  // Wait for transaction confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the asset was created
+  const asset = await fetchAssetV1(umi, assetSigner.publicKey);
+  console.log("Frozen asset created successfully:", assetSigner.publicKey);
+
+  // Step 5: Demonstrate that the asset is truly frozen
+  console.log(
+    "Testing frozen property by attempting a transfer (this should fail)..."
+  );
+  
+  // Attempt to transfer the asset (this will fail due to freeze)
+  const transferResponse = await transfer(umi, {
+    asset: asset,
+    newOwner: DESTINATION_WALLET,
+    collection,
+  }).sendAndConfirm(umi, { send: { skipPreflight: true } });
+
+  // Log the failed transfer attempt signature
+  console.log(
+    "Transfer attempt signature:",
+    base58.deserialize(transferResponse.signature)[0]
+  );
+})();
+
+```
+{% /totem-accordion  %}
+{% /totem %}
+
+### Collection-Level Implementation
+For collections where all assets should be soulbound, applying the plugin at the collection level is more efficient. This requires less rent and enables thawing the entire collection in one transaction.
+
+{% totem %}
+{% totem-accordion title="Code Example" %}
+```js
+import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
+import { mplCore } from "@metaplex-foundation/mpl-core";
+import {
+  generateSigner,
+  keypairIdentity,
+  publicKey,
+  sol,
+} from "@metaplex-foundation/umi";
+import {
+  createCollection,
+  create,
+  fetchCollection,
+  transfer,
+  fetchAssetV1,
+} from "@metaplex-foundation/mpl-core";
+import { base58 } from "@metaplex-foundation/umi/serializers";
+
+// Define a dummy destination wallet for testing transfer restrictions
+const DESTINATION_WALLET = publicKey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d");
+
+(async () => {
+  // Step 1: Initialize Umi with devnet RPC endpoint
+  const umi = createUmi(
+    "YOUR ENDPOINT"
+  ).use(mplCore());
+
+  // Step 2: Create and fund a test wallet
+  const walletSigner = generateSigner(umi);
+  umi.use(keypairIdentity(walletSigner));
+
+  console.log("Funding test wallet with devnet SOL...");
+  await umi.rpc.airdrop(walletSigner.publicKey, sol(0.1));
+  
+  // Wait for airdrop confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Step 3: Create a new frozen collection
+  console.log("Creating frozen collection...");
+  const collectionSigner = generateSigner(umi);
+  await createCollection(umi, {
+    collection: collectionSigner,
+    name: "Frozen Collection",
+    uri: "https://example.com/my-collection.json",
+    plugins: [
+      {
+        // The PermanentFreezeDelegate plugin permanently freezes the collection
+        type: 'PermanentFreezeDelegate',
+        frozen: true, // Set the collection as frozen
+        authority: { type: "None" }, // No authority can unfreeze it
+      },
+    ],
+  }).sendAndConfirm(umi);
+
+  // Wait for collection creation confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the collection was created
+  const collection = await fetchCollection(umi, collectionSigner.publicKey);
+  console.log("Frozen collection created successfully:", collectionSigner.publicKey);
+
+  // Step 4: Create an asset within the frozen collection
+  console.log("Creating asset in frozen collection...");
+  const assetSigner = generateSigner(umi);
+  await create(umi, {
+    asset: assetSigner,
+    collection: collection,
+    name: "Frozen Asset",
+    uri: "https://example.com/my-asset.json",
+  }).sendAndConfirm(umi);
+
+  // Wait for asset creation confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the asset was created
+  const asset = await fetchAssetV1(umi, assetSigner.publicKey);
+  console.log("Asset created successfully in frozen collection:", assetSigner.publicKey);
+
+  // Step 5: Demonstrate that the asset is frozen by the collection
+  console.log(
+    "Testing frozen property by attempting a transfer (this should fail)..."
+  );
+  
+  // Attempt to transfer the asset (this will fail due to collection freeze)
+  const transferResponse = await transfer(umi, {
+    asset: asset,
+    newOwner: DESTINATION_WALLET,
+    collection,
+  }).sendAndConfirm(umi, { send: { skipPreflight: true } });
+
+  // Log the failed transfer attempt signature
+  console.log(
+    "Transfer attempt signature:",
+    base58.deserialize(transferResponse.signature)[0]
+  );
+})();
+
+```
+{% /totem-accordion  %}
+{% /totem %}
+
+## Creating Soulbound NFTs with the Oracle Plugin
+
+The Oracle Plugin provides a way to approve or reject different lifecycle events for an asset. To create soulbound NFTs, we can use a special Oracle deployed by Metaplex that always rejects transfer events while still allowing other operations like burning. This differs from the Permanent Freeze Delegate Plugin approach since assets remain burnable even though they cannot be transferred.
+
+When creating a soulbound asset using the Oracle Plugin, one would attach the plugin to the asset. This can be done on creation or afterwards. In this example we are using a [default Oracle](/core/external-plugins/oracle#default-oracles-deployed-by-metaplex) that will always reject and has been deployed by Metaplex.
+
+This effectively creates a permanently soulbound asset that cannot be transferred but burned. In the following code snippet it is shown how:
+
+```js
+const ORACLE_ACCOUNT = publicKey(
+  "GxaWxaQVeaNeFHehFQEDeKR65MnT6Nup81AGwh2EEnuq"
+);
+
+await create(umi, {
+  asset: assetSigner,
+  collection: collection,
+  name: "My Soulbound Asset",
+  uri: "https://example.com/my-asset.json",
+  plugins: [
+    {
+      // The Oracle plugin allows us to control transfer permissions
+      type: "Oracle",
+      resultsOffset: {
+        type: "Anchor",
+      },
+      baseAddress: ORACLE_ACCOUNT,
+      lifecycleChecks: {
+        // Configure the Oracle to reject all transfer attempts
+        transfer: [CheckResult.CAN_REJECT],
+      },
+      baseAddressConfig: undefined,
+    },
+  ],
+})
+```
+
+### Asset-Level Implementation
+The Oracle Plugin can make individual assets non-transferrable while preserving the ability to burn them. This provides flexibility for cases where assets may need to be destroyed.
+
+{% totem %}
+{% totem-accordion title="Code Example" %}
+```js
+import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
+import { mplCore } from "@metaplex-foundation/mpl-core";
+import {
+  generateSigner,
+  keypairIdentity,
+  publicKey,
+  sol,
+} from "@metaplex-foundation/umi";
+import {
+  createCollection,
+  create,
+  fetchCollection,
+  CheckResult,
+  transfer,
+  fetchAssetV1,
+} from "@metaplex-foundation/mpl-core";
+import { base58 } from "@metaplex-foundation/umi/serializers";
+
+// Define the Oracle account that will control transfer permissions
+// This is an Oracle deployed by Metaplex that always rejects tranferring
+const ORACLE_ACCOUNT = publicKey(
+  "GxaWxaQVeaNeFHehFQEDeKR65MnT6Nup81AGwh2EEnuq"
+);
+
+// Define a dummy destination wallet for testing transfer restrictions
+const DESTINATION_WALLET = publicKey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d");
+
+(async () => {
+  // Step 1: Initialize Umi with devnet RPC endpoint
+  const umi = createUmi(
+    "YOUR ENDPOINT"
+  ).use(mplCore());
+
+  // Step 2: Create and fund a test wallet
+  const walletSigner = generateSigner(umi);
+  umi.use(keypairIdentity(walletSigner));
+
+  console.log("Funding test wallet with devnet SOL...");
+  await umi.rpc.airdrop(walletSigner.publicKey, sol(0.1));
+
+  // Step 3: Create a new collection to hold our soulbound asset
+  console.log("Creating parent collection...");
+  const collectionSigner = generateSigner(umi);
+  await createCollection(umi, {
+    collection: collectionSigner,
+    name: "My Collection",
+    uri: "https://example.com/my-collection.json",
+  }).sendAndConfirm(umi);
+  
+  // Wait for transaction confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the collection was created
+  const collection = await fetchCollection(umi, collectionSigner.publicKey);
+  console.log("Collection created successfully:", collectionSigner.publicKey);
+
+  // Step 4: Create a soulbound asset within the collection
+  console.log("Creating soulbound asset...");
+  const assetSigner = generateSigner(umi);
+  
+  // Create the asset with transfer restrictions using an Oracle plugin
+  await create(umi, {
+    asset: assetSigner,
+    collection: collection,
+    name: "My Soulbound Asset",
+    uri: "https://example.com/my-asset.json",
+    plugins: [
+      {
+        // The Oracle plugin allows us to control transfer permissions
+        type: "Oracle",
+        resultsOffset: {
+          type: "Anchor",
+        },
+        baseAddress: ORACLE_ACCOUNT,
+        lifecycleChecks: {
+          // Configure the Oracle to reject all transfer attempts
+          transfer: [CheckResult.CAN_REJECT],
+        },
+        baseAddressConfig: undefined,
+      },
+    ],
+  }).sendAndConfirm(umi);
+  
+  // Wait for transaction confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the asset was created
+  const asset = await fetchAssetV1(umi, assetSigner.publicKey);
+  console.log("Soulbound asset created successfully:", assetSigner.publicKey);
+
+  // Step 5: Demonstrate that the asset is truly soulbound
+  console.log(
+    "Testing soulbound property by attempting a transfer (this should fail)..."
+  );
+  
+  // Attempt to transfer the asset (this will fail due to Oracle restrictions)
+  const transferResponse = await transfer(umi, {
+    asset: asset,
+    newOwner: DESTINATION_WALLET,
+    collection,
+  }).sendAndConfirm(umi, { send: { skipPreflight: true } });
+
+  // Log the failed transfer attempt signature
+  console.log(
+    "Transfer attempt signature:",
+    base58.deserialize(transferResponse.signature)[0]
+  );
+})();
+
+```
+{% /totem-accordion  %}
+{% /totem %}
+
+### Collection-Level Implementation
+Applying the Oracle Plugin at the collection level makes all assets in the collection non-transferrable but burnable. This is more rent efficient and allows managing permissions for the entire collection at once.
+
+{% totem %}
+{% totem-accordion title="Code Example" %}
+```js
+import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
+import { mplCore } from "@metaplex-foundation/mpl-core";
+import {
+  generateSigner,
+  keypairIdentity,
+  publicKey,
+  sol,
+} from "@metaplex-foundation/umi";
+import {
+  createCollection,
+  create,
+  fetchCollection,
+  CheckResult,
+  transfer,
+  fetchAssetV1,
+} from "@metaplex-foundation/mpl-core";
+import { base58 } from "@metaplex-foundation/umi/serializers";
+
+// Define the Oracle account that will control transfer permissions
+// This is an Oracle deployed by Metaplex that always rejects transferring
+const ORACLE_ACCOUNT = publicKey(
+  "GxaWxaQVeaNeFHehFQEDeKR65MnT6Nup81AGwh2EEnuq"
+);
+
+// Define a dummy destination wallet for testing transfer restrictions
+const DESTINATION_WALLET = publicKey("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d");
+
+(async () => {
+  // Step 1: Initialize Umi with devnet RPC endpoint
+  const umi = createUmi(
+    "YOUR ENDPOINT"
+  ).use(mplCore());
+
+  // Step 2: Create and fund a test wallet
+  const walletSigner = generateSigner(umi);
+  umi.use(keypairIdentity(walletSigner));
+
+  console.log("Funding test wallet with devnet SOL...");
+  await umi.rpc.airdrop(walletSigner.publicKey, sol(0.1));
+  
+  // Wait for airdrop confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Step 3: Create a new collection with transfer restrictions
+  console.log("Creating soulbound collection...");
+  const collectionSigner = generateSigner(umi);
+  await createCollection(umi, {
+    collection: collectionSigner,
+    name: "Soulbound Collection",
+    uri: "https://example.com/my-collection.json",
+    plugins: [
+      {
+        // The Oracle plugin allows us to control transfer permissions
+        type: "Oracle",
+        resultsOffset: {
+          type: "Anchor",
+        },
+        baseAddress: ORACLE_ACCOUNT,
+        lifecycleChecks: {
+          // Configure the Oracle to reject all transfer attempts
+          transfer: [CheckResult.CAN_REJECT],
+        },
+        baseAddressConfig: undefined,
+      },
+    ],
+  }).sendAndConfirm(umi);
+
+  // Wait for collection creation confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the collection was created
+  const collection = await fetchCollection(umi, collectionSigner.publicKey);
+  console.log("Soulbound collection created successfully:", collectionSigner.publicKey);
+
+  // Step 4: Create a soulbound asset within the collection
+  console.log("Creating soulbound asset...");
+  const assetSigner = generateSigner(umi);
+  await create(umi, {
+    asset: assetSigner,
+    collection: collection,
+    name: "Soulbound Asset",
+    uri: "https://example.com/my-asset.json",
+  }).sendAndConfirm(umi);
+
+  // Wait for asset creation confirmation
+  await new Promise(resolve => setTimeout(resolve, 15000));
+
+  // Fetch and verify the asset was created
+  const asset = await fetchAssetV1(umi, assetSigner.publicKey);
+  console.log("Soulbound asset created successfully:", assetSigner.publicKey);
+
+  // Step 5: Demonstrate that the asset is truly soulbound
+  console.log(
+    "Testing soulbound property by attempting a transfer (this should fail)..."
+  );
+  
+  // Attempt to transfer the asset (this will fail due to Oracle restrictions)
+  const transferResponse = await transfer(umi, {
+    asset: asset,
+    newOwner: DESTINATION_WALLET,
+    collection,
+  }).sendAndConfirm(umi, { send: { skipPreflight: true } });
+
+  // Log the failed transfer attempt signature
+  console.log(
+    "Transfer attempt signature:",
+    base58.deserialize(transferResponse.signature)[0]
+  );
+})();
+
+```
+{% /totem-accordion  %}
+{% /totem %}

+ 2 - 0
src/pages/core/guides/index.md

@@ -8,6 +8,8 @@ The following Guides for MPL Core are currently available:
 
 {% quick-links %}
 
+{% quick-link title="Soulbound NFT" icon="CodeBracketSquare" href="/core/guides/create-soulbound-nft-asset" description="Different options for Soulbound NFT including code examples" /%}
+
 {% quick-link title="Print Editions" icon="CodeBracketSquare" href="/core/guides/print-editions" description="Learn how to combine plugins to create Editions with MPL Core" /%}
 
 {% quick-link title="Immutability" icon="BookOpen" href="/core/guides/immutability" description="Learn how Immutability works in MPL Core" /%}