User Update Example

This guide provides a complete Deno implementation for user-driven CIP-68 NFT metadata updates using spend validators, fee payments, and token ownership verification with the Anvil API.

Want to understand the smart contract validation logic first? Check out the Smart Contract Logic Guide to learn how the contract validates user updates. 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 user-driven metadata updates where token holders can update their own NFT metadata by:

  • Proving ownership of the user token (222)

  • Paying a 1 ADA fee to the admin

  • Updating limited metadata fields (e.g., nickname)

  • Following the spend validator's authorization rules

Prerequisites

This guide assumes you have completed the Minting Example and have:

  1. Existing CIP-68 tokens - Reference and user tokens from a successful mint

  2. User token ownership - The user token (222) must be in the customer's wallet

  3. Reference token location - Current UTXO of the reference token at the script address

  4. Blockfrost API access - For dynamic UTXO discovery

  5. Understanding of user updates - Review update logic for validation details

Step-by-Step Implementation

Let's build a user-driven CIP-68 metadata update script step by step. Each section explains the concept and shows the corresponding code.

Step 1: Setup and Configuration

Create update file with the required imports and configuration:

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

// Import wallet files and constants
import customer from "./wallet-customer.json" with { type: "json" };
import adminWallet from "./wallet-mint-sc-policy.json" with { type: "json" };
import { API_URL, HEADERS } from "../../../utils/constant.ts";

// Configuration
const CUSTOMER_ADDRESS = customer.base_address_preprod;
const ADMIN_KEY_HASH = adminWallet.key_hash;
const ADMIN_ADDRESS = adminWallet.base_address_preprod;
const assetName = "cip68_1753999309623"; // Your minted asset name
const nickname = "customer_updated"; // New nickname to set

// Import Blockfrost utility for dynamic UTXO fetching
import { 
  getUtxos, 
  findUserTokenUTXO, 
  findReferenceTokenUTXO 
} from "../../../fetch-utxos-from-the-backend/utxos/blockfrost.ts";

// Blockfrost configuration
const BLOCKFROST_BASE_URL = "https://cardano-preprod.blockfrost.io/api/v0";
const BLOCKFROST_API_KEY = Deno.env.get("BLOCKFROST_PROJECT_ID");

if (!BLOCKFROST_API_KEY) {
  console.error("❌ BLOCKFROST_PROJECT_ID environment variable is required");
  Deno.exit(1);
}

Key differences from admin updates:

  • Customer wallet signs the transaction (not admin)

  • Admin address receives the 1 ADA fee payment

  • Blockfrost integration for dynamic UTXO discovery

  • Asset name must match the originally minted token

Step 2: Apply Parameters and Find UTXOs

Apply parameters to the blueprint and locate the required UTXOs:

// Load blueprint and apply parameters (same as other examples)
import blueprint from "../aiken-mint-cip-68/plutus.json" with { type: "json" };

console.log("🔄 Applying parameters to blueprint...");
const applyParamsResponse = await fetch(`${API_URL}/blueprints/apply-params`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    params: { [blueprint.validators[0].hash]: [ADMIN_KEY_HASH] },
    blueprint: blueprint
  }),
});

const applyParamsResult = await applyParamsResponse.json();
const parameterizedScript = applyParamsResult.preloadedScript;
const POLICY_ID = Object.keys(applyParamsResult.addresses)[0];
const SC_ADDRESS = applyParamsResult.addresses[POLICY_ID].bech32;

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

// Find reference token UTXO at script address
const referenceTokenUTXO = await findReferenceTokenUTXO(
  BLOCKFROST_BASE_URL,
  BLOCKFROST_API_KEY,
  POLICY_ID,
  assetName,
  SC_ADDRESS
);

// Find user token UTXO at customer address
const userTokenUTXO = await findUserTokenUTXO(
  BLOCKFROST_BASE_URL,
  BLOCKFROST_API_KEY,
  POLICY_ID,
  assetName,
  CUSTOMER_ADDRESS
);

// Get user token UTXO in string format for requiredInputs
const utxos = await getUtxos(BLOCKFROST_BASE_URL, BLOCKFROST_API_KEY, CUSTOMER_ADDRESS);
const utxoString = `000de140${Buffer.from(assetName).toString("hex")}`;
const userUtxo = utxos.find((utxo) => utxo.includes(utxoString));

if (!userUtxo) {
  console.error("❌ User token UTXO not found at customer address");
  Deno.exit(1);
}

console.log(`Reference token: ${referenceTokenUTXO.transaction_id}#${referenceTokenUTXO.output_index}`);
console.log(`User token: ${userTokenUTXO.transaction_id}#${userTokenUTXO.output_index}`);

What's happening here:

  • Reference token discovery: Finds the current reference token UTXO at the script address

  • User token discovery: Locates the user token in the customer's wallet (proves ownership)

  • UTXO string format: Gets the hex-encoded UTXO string needed for requiredInputs

  • Validation: Ensures both tokens exist before proceeding

Step 3: Build the User Update Transaction

Now we build the spend transaction with user authorization:

const input = {
  changeAddress: CUSTOMER_ADDRESS,
  message: "CIP-68 Customer Update Example",
  
  // SCRIPT INTERACTIONS: Tell the validator how to spend the reference token
  scriptInteractions: [
    {
      purpose: "spend", // Spending a UTXO locked at smart contract address
      
      // REFERENCE TOKEN UTXO: The UTXO containing the reference token to update
      outputRef: {
        txHash: referenceTokenUTXO.transaction_id,
        index: referenceTokenUTXO.output_index,
      },
      
      // REDEEMER: Authorization data for the spend validator
      redeemer: {
        type: "json",
        value: {
          output_index: 0, // Index of output containing updated reference token
          update: {
            // USER UPDATE: Triggers UserUpdate validation branch
            nickname: Buffer.from(nickname).toString("hex"), // New nickname in hex
            fee_output_index: 1, // Index of output paying 1 ADA fee to admin
            
            // USER TOKEN REFERENCE: Validator uses this to verify ownership
            output_ref_user_token: {
              transaction_id: userTokenUTXO.transaction_id,
              output_index: userTokenUTXO.output_index,
            },
          },
        },
      },
    },
  ],

Key redeemer components:

  • nickname: The updated value in hex format (limited user permission)

  • fee_output_index: Points to the 1 ADA fee payment output

  • output_ref_user_token: References the user token for ownership verification

Step 4: Define Transaction Outputs

Create the outputs for the updated reference token and fee payment:

  // TRANSACTION OUTPUTS: What the transaction creates
  outputs: [
    {
      // OUTPUT 0: Updated reference token back to script address
      address: SC_ADDRESS,
      assets: [
        {
          assetName: { name: assetName, label: 100, format: "utf8" },
          policyId: POLICY_ID,
          quantity: 1,
        },
      ],
      // UPDATED DATUM: Contains new metadata with updated nickname
      datum: {
        type: "inline",
        shape: {
          validatorHash: POLICY_ID,
          purpose: "spend",
        },
        value: {
          metadata: [
            ["name", assetName], // Must match original minted asset name
            ["nickname", "customer_updated"], // Updated field
          ],
          version: 1,
        },
      },
    },
    {
      // OUTPUT 1: 1 ADA fee payment to admin
      address: ADMIN_ADDRESS,
      lovelace: "1000000", // Exactly 1 ADA - validator verifies this amount
    },
  ],

  requiredSigners: [], // No admin signature required for user updates
  referenceInputs: [],
  requiredInputs: [userUtxo!], // Include user token as input (proves ownership)
  preloadedScripts: [parameterizedScript],
};

Critical output requirements:

  • Output 0: Updated reference token with new metadata at script address

  • Output 1: Exactly 1 ADA fee payment to admin address

  • Datum preservation: Original asset name must be preserved

  • User token input: Must be included via requiredInputs for ownership proof

Step 5: Build, Sign, and Submit Transaction

Build and submit the user update transaction:

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

if (!contractDeployed.ok) {
  const errorText = await contractDeployed.text();
  console.error("❌ Transaction build failed:", errorText);
  Deno.exit(1);
}

const transaction = await contractDeployed.json();
console.log("✅ Transaction built successfully");

// Sign with customer's private key (not admin)
const txToSubmitOnChain = FixedTransaction.from_bytes(
  Buffer.from(transaction.complete, "hex")
);
txToSubmitOnChain.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(customer.skey)
);

// Submit to network
const submitted = await fetch(`${API_URL}/transactions/submit`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ transaction: txToSubmitOnChain.to_hex() }),
});

const output = await submitted.json();
if (!submitted.ok) {
  console.error("❌ Transaction submission failed:", output);
  Deno.exit(1);
}

console.log("✅ Customer update transaction submitted successfully");
console.log("Transaction hash:", output.transactionId);

What's happening here:

  • Customer signature: Only the customer signs (proves they own the user token)

  • No admin signature: Admin authorization comes from fee payment, not signature

  • Validation on-chain: Smart contract validates ownership and fee payment

Running the Example

Prerequisites Setup

  1. Complete the minting example first to have CIP-68 tokens available

  2. Set up environment variables:

    # PowerShell/Windows
    $env:BLOCKFROST_PROJECT_ID='your_blockfrost_api_key'
    
    # Bash/macOS/Linux
    export BLOCKFROST_PROJECT_ID='your_blockfrost_api_key'
  3. Update configuration:

    • Set assetName to match your minted token

    • Modify nickname to your desired update value

    • Ensure wallet files are properly configured

  4. Run the user update script:

    deno run --allow-all user-update.ts

Expected Output

🔄 Applying parameters to blueprint...
✅ Parameters applied successfully
Parameterized script hash: 4983663c2b236161ad8e26c36dff9aee709a6adef53be2...
Script address: addr_test1wpycxe3u9v3kzcdd3cnvxm0lnth8pxn2mm6nhch9f6pagxq...
Reference token: 1750a1c191646ade084c911fd9fd8a7c6b8372923e2305c4f4...#0
User token: 1750a1c191646ade084c911fd9fd8a7c6b8372923e2305c4f4...#1
✅ Transaction built successfully
✅ Customer update transaction submitted successfully
Transaction hash: b2cb92868e58238edb5646775847fca911c46c4d2206bb66b64c4df0af06187b

User vs Admin Updates

This user update approach differs from admin updates in three key ways:

Aspect
User Updates
Admin Updates

Authorization

Token ownership + 1 ADA fee

Admin signature only

Permissions

Limited fields (e.g., nickname)

Full metadata access

Cost

1 ADA fee to admin

Free for admin

The economic barrier prevents spam while giving users control over their metadata.

Troubleshooting

Common Validation Failures

If your transaction fails validation, check these common issues:

  • user_token_quantity != 1 - User token not included in transaction inputs

  • lovelace_paid != 1000000 - Fee amount is not exactly 1 ADA

  • !is_paid_to_admin - Fee payment sent to wrong address

  • current_datum != expected_datum - Attempting to update restricted metadata fields

For detailed validator logic, see the Update Logic Guide.

Key Requirements

  • Include user token in requiredInputs for ownership proof

  • Pay exactly 1 ADA (1,000,000 lovelace) to admin address

  • Update only permitted fields as defined by the smart contract

  • Track UTXO locations dynamically (tokens move with each update)

Next Steps

You now have a complete user-driven update implementation. For production applications, consider:

  • Batch Updates: Multiple metadata fields in one transaction

  • Update History: Track metadata changes for audit trails

  • Fee Estimation: Display costs before transaction submission

  • User Interface: Build a frontend for non-technical users

References

Last updated

Was this helpful?