Mint Example

This guide provides a complete Deno implementation for minting CIP-68 NFTs using parameterized smart contracts with the Anvil API.

Want to understand the smart contract validation logic first? Check out the Mint Logic Guide to learn how the contract ensures CIP-68 compliance. Otherwise, you can dive right into the implementation below!

Full Code Example

Want to see the complete code first? Check out the full working implementation on GitHub, then come back here for the step-by-step breakdown.

Implementation Overview

This example demonstrates:

  • Applying parameters to uploaded blueprints

  • Building transactions with preloaded scripts

  • Dual-wallet signing for smart contract authorization

  • Complete end-to-end minting workflow

Prerequisites

Before running this example, ensure you have:

  1. Deno installed - Download here

  2. Anvil API key - Get one from the Anvil dashboard

  3. Two wallets with testnet ADA: (See Wallet CLI)

    • Customer wallet (pays fees, receives user token)

    • Admin wallet (provides key_hash parameter, signs transaction)

  4. Uploaded CIP-68 blueprint - The original blueprint must be uploaded to the Anvil API first. See blueprint Management

  5. Understanding of parameterized contracts - Parameters are applied at transaction time, not deployment time

Step-by-Step Implementation

Let's build a parameterized CIP-68 minting script step by step. Each section will explain the concept and show the corresponding code.

Step 1: Setup and Imports

First, we need to import the required dependencies and load our configuration files.

Create cip68-parameterized-mint.ts:

import { Buffer } from "node:buffer";
import {
  FixedTransaction,
  PrivateKey,
} from "npm:@emurgo/[email protected]";

// Import wallet configurations and blueprint
import customer from "./wallet-customer.json" with { type: "json" };
import adminWallet from "./wallet-mint-sc-policy.json" with { type: "json" };
import blueprint from "../aiken-mint-cip-68/plutus.json" with { type: "json" };
import { API_URL, HEADERS } from "../../../utils/constant.ts";

// Configuration - Customize these values for your project
const CUSTOMER_ADDRESS = customer.base_address_preprod; // Wallet that pays fees and receives user token
const ADMIN_KEY_HASH = adminWallet.key_hash; // Admin key for smart contract authorization
const ASSET_NAME = "test"; // Base name for your CIP-68 token pair

What's happening here:

  • We import Cardano Serialization Library for transaction signing

  • Load wallet files (customer pays fees, admin provides authorization)

  • Load the original blueprint (compiled from Aiken smart contract)

  • Set up API configuration and asset name

Step 2: Apply Parameters to Blueprint

The blueprint that we uploaded is generic. In order to make it custom for this collection. We need to apply the parameters to the smart contract, parameterized contracts are customized at transaction time. We apply the admin's key hash to create a personalized version of the contract:


console.log("πŸ”„ Applying parameters to blueprint...");
const originalHash = blueprint.validators[0].hash;
const applyParamsResponse = await fetch(
  `${API_URL}/blueprints/apply-params`,
  {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      params: { [originalHash]: [ADMIN_KEY_HASH] }, // Apply admin key to parameterize contract
      blueprint: blueprint
    }),
  },
);

if (!applyParamsResponse.ok) {
  console.error("❌ Failed to apply parameters:", applyParamsResponse.status, applyParamsResponse.statusText);
  const errorText = await applyParamsResponse.text();
  console.error("Error details:", errorText);
  Deno.exit(1);
}

const applyParamsResult = await applyParamsResponse.json();
console.log("βœ… Parameters applied successfully");

// Extract the parameterized script and address
const parameterizedScript = applyParamsResult.preloadedScript;
const newHashes = Object.keys(applyParamsResult.addresses);
const SCRIPT_HASH = newHashes[0];
const SC_ADDRESS = applyParamsResult.addresses[SCRIPT_HASH].bech32;

console.log("Parameterized script hash:", SCRIPT_HASH);
console.log("Script address:", SC_ADDRESS);

What's happening here:

  • We call /blueprints/apply-params with the original blueprint and admin key hash

  • The endpoint generates a new script with the admin key "baked in" as a parameter

  • We get back a new script hash, address, and complete parameterized script

  • Important: This parameterized script is NOT saved to the database - we must use it directly

Expected output:

πŸ”„ Applying parameters to blueprint...
βœ… Parameters applied successfully
Parameterized script hash: 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d6499
Script address: addr_test1wpw62p4shrvu0kwly378dp9w4k8tdldvrjlm9htucv7kfxggmdaj6

Step 3: Build the Transaction Payload

Now we build the CIP-68 minting transaction. The key insight is that we include the parameterized script as a preloadedScript so the tx-builder can validate it:


const input = {
  changeAddress: CUSTOMER_ADDRESS,
  //utxos: [], // Required for LIVE network - provide actual UTxOs
  message: "Cip68 example",
  
  // Mint both reference and user tokens
  mint: [
    {
      // Reference token -> This is the token that will be spent to update metadata
      version: "cip68",
      assetName: {name: assetName, label: 100, format: "utf8"}, 
      policyId: SCRIPT_HASH,
      type: "plutus",
      quantity: 1,
    },
    {
      // User token -> This is the token that will be sent to the user
      version: "cip68",
      assetName: {name: assetName, label: 222, format: "utf8"}, 
      policyId: SCRIPT_HASH,
      type: "plutus",
      quantity: 1,
    },
  ],

  // Script Interactions - Specify redeemer data for smart contract validation during minting
  scriptInteractions: [
    {
      purpose: "mint",
      hash: SCRIPT_HASH,
      redeemer: {
        type: "json",
        value: {
          // Specify the asset name to be minted - Used to validate that the minting transaction
          // is minting the intended CIP-68 assets.
          assetnames: [[Buffer.from(assetName).toString("hex"), 0]]
        },
      },
    },
  ],
  
  // Override Mint Array's 'reference token' (Label 100) output   
  // to modify the datum as spendable in case of a metadata update.
  // This will be sent to the smart contract address.
  outputs: [
    {
      address: SC_ADDRESS, 
      assets: [
        {
          assetName: { name: ASSET_NAME, label: 100, format: "utf8" }, // Reference token (100)
          policyId: SCRIPT_HASH,
          quantity: 1,
        },
      ],
      datum: {
        type: "inline",
        shape: {
          validatorHash: SCRIPT_HASH,
          purpose: "spend",
        },
        value: {
          metadata: [
            ["name", ASSET_NAME], // Must match asset name for CIP-68 compliance
            ["nickname", "test_nickname"], // Custom metadata field
          ],
          version: 1
        }
      },
    },
  ],
  
  requiredSigners: [ADMIN_KEY_HASH], // Admin must sign
  preloadedScripts: [parameterizedScript]
};

What's happening here:

  • Mint array: Creates both reference (100) and user (222) tokens with the same base name

  • Script interactions: Tells the smart contract which assets we're minting via the redeemer

  • Outputs: Explicitly routes the reference token to the script address with metadata datum

  • Required signers: Ensures the admin (whose key was used in parameters) signs the transaction

  • Preloaded scripts: Includes our parameterized script so tx-builder can validate it

Key insight: The user token (222) automatically goes to the change address, while the reference token (100) needs an explicit output with custom datum in order to go to the script address.

Step 4: Build the Transaction

Finally, we build the transaction:

// Build the transaction
console.log("πŸ”¨ Building transaction...");
const contractDeployed = await fetch(`${API_URL}/transactions/build`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify(input),
});

if (!contractDeployed.ok) {
  console.error("❌ Failed to build transaction:", contractDeployed.status, contractDeployed.statusText);
  const errorText = await contractDeployed.text();
  console.error("Error details:", errorText);
  Deno.exit(1);
}

const contractResult = await contractDeployed.json();
console.log("βœ… Transaction built successfully");

What's happening here:

  • The tx-builder validates our payload and creates a Cardano transaction

  • It includes the parameterized script from our preloadedScripts

  • The transaction is returned as a hex-encoded string ready for signing

Step 5: Sign with Both Wallets

CIP-68 smart contracts require signatures from both the customer (who pays fees) and the admin (who authorizes minting):

// Sign the transaction using CSL.
const txToSubmitOnChain = FixedTransaction.from_bytes(Buffer.from(contractResult.complete, "hex"));
txToSubmitOnChain.sign_and_add_vkey_signature(PrivateKey.from_bech32(customer.skey));
txToSubmitOnChain.sign_and_add_vkey_signature(PrivateKey.from_bech32(adminWallet.skey));

console.log("βœ… Transaction signed with both keys");

What's happening here:

  • We deserialize the transaction from hex format

  • Load both private keys (customer and admin)

  • Add both signatures to the witness set

  • Create the final signed transaction

Step 6: Submit to Network

Finally, we submit the signed transaction to the Cardano network:

// Submit to network
console.log("πŸ“‘ Submitting to network...");
const submitResponse = await fetch(`${API_URL}/transactions/submit`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ transaction: signedTx.to_hex() }),
});

if (!submitResponse.ok) {
  console.error("❌ Failed to submit transaction:", submitResponse.status, submitResponse.statusText);
  const errorText = await submitResponse.text();
  console.error("Error details:", errorText);
  Deno.exit(1);
}

const submitResult = await submitResponse.json();
console.log("πŸŽ‰ Transaction submitted successfully!");
console.log("Transaction hash:", submitResult.transactionId);
console.log("Reference Token (100):", `${SCRIPT_HASH}.${Buffer.from(assetName).toString("hex")}64`);
console.log("User Token (222):", `${SCRIPT_HASH}.${Buffer.from(assetName).toString("hex")}de`);

What's happening here:

  • The signed transaction is submitted to the Cardano network

  • On success, we get back a transaction hash for tracking

  • Both CIP-68 tokens are now minted and distributed according to our rules

Expected output:

πŸ“‘ Submitting to network...
πŸŽ‰ Transaction submitted successfully!
Transaction hash: a1b2c3d4e5f6789...
Reference Token (100): 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d649974657374064
User Token (222): 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d649974657374de

Running the Example

Prerequisites Setup

  1. Upload the blueprint first using a separate upload script:

    deno run --allow-all upload-blueprint.ts --blueprint=../aiken-mint-cip-68/plutus.json
  2. Set up wallet files:

    • wallet-customer.json - Customer wallet (pays fees, receives user token)

    • wallet-mint-sc-policy.json - Admin wallet (provides key_hash parameter)

  3. Update configuration:

    • Ensure API_URL and HEADERS are configured in your constants file

    • Modify assetName and metadata as desired

  4. Run the parameterized minting script:

    deno run --allow-all cip68-parameterized-mint.ts

Expected Output

πŸ”„ Applying parameters to blueprint...
βœ… Parameters applied successfully
Parameterized script hash: 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d6499
Script address: addr_test1wpw62p4shrvu0kwly378dp9w4k8tdldvrjlm9htucv7kfxggmdaj6
βœ… Transaction built successfully
πŸŽ‰ CIP-68 Asset Minted Successfully!
Transaction Hash: 74dcb95427b9eee36137803617a900460b5d58ed266598ebf4146d659b613520
Reference Token (100): 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d6499.74657374064
User Token (222): 5da506b0b8d9c7d9df247c7684aead8eb6fdac1cbfb2dd7cc33d6499.746573740de
βœ… Transaction submitted successfully

Next Steps

This example demonstrates the complete parameterized CIP-68 workflow. For production use:

  1. Add robust error handling for apply-params and transaction failures

  2. Implement parameter validation to ensure correct admin keys

  3. Add transaction monitoring to track minting status on-chain

  4. Create reusable functions for parameter application and script generation

  5. Implement batch operations for multiple asset minting

References

Last updated

Was this helpful?